I am trying to build a countdown timer app with start/pause button using React hooks.
However, the interaction does not responds as expected.
As the screenshot of console.log shows here:
In Event 2, when pause button gets clicked, props.setIsPaused(true) should set isPaused true, but console.log shows false. However, the value of isPaused in parent component gets updated to true according to console.log.
In Event 3, when start button gets clicked, props.setIsPaused(false) should set isPaused false, but console.log shows true. However, the value of isPaused in parent component gets updated to false according to console.log.
In this case, countdown timer does not start, I need to click start button for 2nd time to have it finally start the countdown.
I am totally confused by these actions out of my expectations.
Any ideas or thoughts what I missed or mistakes I have?
Thank you.
Here is the code of the child/parent component performing timer countdown:
// Child Component
function TimerPanel(props) {
const launch = React.useRef();
var secLeft = parseInt(props.timerNow.min) * 60 + parseInt(props.timerNow.sec)
const startCountDown = () => {
props.setIsPaused(false) // Once start button is clicked, set isPaused false
console.log('props.isPaused inside startCountDown func: ' + props.isPaused) // This should be false
launch.current = setInterval(decrement, 1000)
}
function decrement() {
if (secLeft > 0 && !props.isPaused) {
console.log('props.isPaused inside decrement func: '+ props.isPaused)
secLeft--;
var minCountDown = Math.floor(secLeft / 60)
var secCountDown = Math.floor((secLeft % 60) * 100) / 100
props.setTimerNow({
...props.timerNow,
min: minCountDown,
sec: secCountDown})
}
}
const pauseCountDown = () => {
props.setIsPaused(true) // Once pause button is clicked, set isPaused true
console.log('props.isPaused inside pauseCountDown func: '+ props.isPaused) // This should be true
clearInterval(launch.current)
}
......
}
// Parent Component
function App() {
const [breakLength, setBreakLength] = React.useState(5)
const [sessionLength, setSessionLength] = React.useState(25)
const [title, setTitle] = React.useState('Session')
const initialTimer = {
min: 25,
sec: 0
}
const [timerNow, setTimerNow] = React.useState(initialTimer)
const [isPaused, setIsPaused] = React.useState(false)
console.log(timerNow)
console.log('isPaused in App: ' + isPaused)
..........
}
Issues
I think what may've been tripping you up (I know it was me) was the split of logic between the app component and the timer panel component. All the state was declared in App, but all the state was mutated/updated in children components. I think some state was also poorly named, which made working with it a little confusing.
I've directly answered your concerns about the reflected values of isPaused being "delayed" in the comments, but I wanted to see how I could help.
Suggested Solution
Move all the logic to manipulate the timer state into the same component where the state is declared, pass functions to children to callback to "inform" the parent to do some "stuff".
In the following example I was able to greatly reduce the burden of TimerPanel trying to compute the time to display and trying to update it and keep in sync with the paused state from the parent.
TimerPanel
Receives timer value, in seconds, and computes the displayed minutes and values since this is easily derived from state. Also receives callbacks to start/stop/reset the timer state.
function TimerPanel({
timerNow,
title,
startTimer,
pauseTimer,
resetTimer,
}) {
const minutes = Number(Math.floor(timerNow / 60))
const seconds = Number(Math.floor((timerNow % 60) * 100) / 100)
return (
<div className="timer-wrapper">
<div className="display-title">{title}</div>
<div className="display-digits">
{minutes.toString().padStart(2, "0")}:
{seconds.toString().padStart(2, "0")}
</div>
<div className="control-keysets">
<i className="fas fa-play-circle" onClick={startTimer}>
Play
</i>
<i className="fas fa-pause-circle" onClick={pauseTimer}>
Pause
</i>
<button className="reset" onClick={resetTimer}>
Reset
</button>
</div>
</div>
);
}
App
Simply timer state to be just time in seconds as this makes time management much simpler. Create handlers to start, pause, and reset the timer state. Use an useEffect hook to run the effect of starting/stopping the time based on the component state. Do correct state logging in an useEffect hook with appropriate dependency.
function App() {
const [breakLength, setBreakLength] = React.useState(5);
const [sessionLength, setSessionLength] = React.useState(25);
const [title, setTitle] = React.useState("Session");
const initialTimer = 60 * 25;
const [timerNow, setTimerNow] = React.useState(initialTimer);
const [isStarted, setIsStarted] = React.useState(false);
const timerRef = React.useRef();
const start = () => setIsStarted(true);
const pause = () => setIsStarted(false);
const reset = () => {
setIsStarted(false);
setTimerNow(initialTimer);
}
React.useEffect(() => {
console.log("isStarted in App: " + isStarted);
if (isStarted) {
timerRef.current = setInterval(() => setTimerNow(t => t - 1), 1000);
} else {
clearInterval(timerRef.current)
}
}, [isStarted]);
React.useEffect(() => {
console.log('timer', timerNow);
}, [timerNow])
return (
<div className="container">
<h1>Pomodoro Timer</h1>
<div className="group">
<div className="block">
<BreakLength
breakLength={breakLength}
setBreakLength={setBreakLength}
isPaused={!isStarted}
/>
</div>
<div className="block">
<SessionLength
sessionLength={sessionLength}
setSessionLength={setSessionLength}
timerNow={timerNow}
setTimerNow={setTimerNow}
isPaused={!isStarted}
/>
</div>
</div>
<div className="bottom-block">
<TimerPanel
title={title}
setSessionLength={setSessionLength}
timerNow={timerNow}
startTimer={start}
pauseTimer={pause}
resetTimer={reset}
/>
</div>
</div>
);
}
I left the BreakLength and SessionLength components intact as they don't appear to be utilized much yet.
Demo
Related
I'm using React to Create an img element that changes its src attribute after specific time with setInterval function based on an array that contains many images.
so the thing is i want to make an animation whenever the src attribute changes, i was thinking of using React.useEffect() hook to watch the src changes and add some animation but i couldn't go further logically.
my code :
import Canada from "../images/Canada.jpg"
import Tokyo from "../images/Tokyo.jpg"
import NewYork from "../images/NewYork.jpg"
import southKorea from "../images/southKorea.jpg"
function AboutMe() {
const imgArray = [NewYork, Tokyo, Canada, southKorea]
let i = 0;
const maxImgArray = imgArray.length -1 ;
const swap = function() {
let image = imgArray[i];
i = (i === maxImgArray) ? 0 : ++i;
const attr = document.getElementById("moving-image");
attr.src=image;
}
setInterval(swap, 5000)
return (
<section className="About-me">
<h1>About me</h1>
<div className="container">
<div className="cities-images">
<img
src={Tokyo}
alt="NewYork" id="moving-image"></img>
<label class="switch">
<input type="checkbox"/>
<span class="slider round"></span>
</label>
</div>
<div className="info">
</div>
</div>
</section>
)
}
By using useState hook in react, we can re-render the component with updated value. So for src change, we can use useState to update the current src of the image.
By using useEffect hook we can do anything once src state change like set different animations.
Component State
const [image, setImage] = useState(Usa);
const [imageIndex, setImageIndex] = useState(0);
const imgArray = [Usa, Japan, Canada, SouthKorea];
useEffect
useEffect(() => {
let interval = null;
if (i !== maxImgArray) {
interval = setInterval(() => {
let image = imgArray[imageIndex];
const attr = document.getElementById("moving-image");
attr.src = image;
// update the img index to state
setImageIndex((imageIndex) =>
imageIndex === maxImgArray ? 0 : imageIndex + 1
);
// update the src in state.
setImage(attr.src);
}, 3000);
}
// When our code runs and reruns for every render, useEffect also cleans up after itself using the cleanup function.
// Here we clear the interval to remove the effects that could happen with state change while interval.
return () => clearInterval(interval);
}, [image, imageIndex]); // when image, imageIndex gets change, useEffect will be triggered.
Component here I used framer-motion for animation.
<section className="Country-flag">
<h1>Country Flag</h1>
<div className="container">
<motion.div
key={image}
animate={{ x: 100, opacity: 1 }}
transition={{
delay: 1,
x: { type: "spring", stiffness: 100 },
default: { duration: 2 }
}}
>
{/* when state {image} value change, this component will re-render with new src */}
<img alt="NewYork" src={image} id="moving-image"></img>
</motion.div>
<div className="info"></div>
</div>
</section>
Check this sample sandbox for produced code.
If you want different animations for each img, then add another state variable for animation and change the value inside the useEffect for each image state.
I want to change the count by one and make the same button revert back to its previous state on click.
export const CountButton = () => {
previousState = 100;
const [count, setCount] = useState(false);
return(
<div>
<button onClick={() => setCount(!count)}>Click Me!</button>
<div>{count ? previousState + 1 : previousState }</div>
</div>
)
}
This code will increase the vote by one and then revert on the second click.
I created a react component that is made of a 3d cube (using css) and 2 buttons that rotate this cube 90 degrees (1 button rotates the cube left and the other one right)
import React from 'react';
import './App.css';
function App() {
const rotateLeft = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
const newNode = boxArea.cloneNode(true);
boxArea.classList.add('rotateLeft');
setTimeout(function(){
boxArea.parentNode.replaceChild(newNode, boxArea);
}, 3005)
};
const rotateRight = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
const newNode = boxArea.cloneNode(true);
boxArea.classList.add('rotateRight');
setTimeout(function(){
boxArea.parentNode.replaceChild(newNode, boxArea);
}, 3005)
};
return (
<div className="App">
<div className="hexagon-panel">
<div className="wrapper">
<div className="box-area">
<div className="box front"/>
<div className="box back"/>
<div className="box left"/>
<div className="box right"/>
</div>
</div>
<div className="hexagon-actions">
<button onClick={rotateLeft}>Left</button>
<button onClick={rotateRight}>Right</button>
</div>
</div>
</div>
);
}
export default App;
As you can see, I trigger the animation by adding the class, I wait a bit more than 3 seconds (the animation lasts 3 seconds) and then I replace the Node with one that doesn't contain the class.
A better solution (but I don't like it either) is to remove the class after the animation ends.
const rotateLeft = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
boxArea.classList.add('rotateLeft');
setTimeout(function(){
boxArea.classList.remove('rotateLeft');
}, 4000)
};
But I am not convinced. The reason for this is the setTimeout since it is a global function I am afraid of a memory leak (even if it is a small one). Calling a new setTimeout on every click and not erasing the previous one.
I was thinking about this
const rotateLeft = async (event) => {
event.preventDefault();
const boxArea = document.getElementsByClassName("box-area")[0];
boxArea.classList.add('rotateLeft');
const func = setTimeout(function(){
boxArea.classList.remove('rotateLeft');
}, 4000)
clearTimeout(func);
};
But I don't know if that even makes sense.
Is there a way a better way to remove the class without the use of setTimeout? Or in the case that it is needed, what would be a good practice to delete it?
Thank you for your time!
You might be looking for this: Using Animation Events. It includes the animationend event that fires when the animation ends.
Or in the future, you could use the Web Animations API once it is widely implemented.
Or if you want to stick with setTimeout, just put the cleanup code (clearTimeout, remove class, etc.) before starting the animation. This way, you always get a 'clean' state to work with.
Im starting with react and im trying to make a horizontal scrolling page. It seems to work just fine except for one thing, of which i'm pretty certain i'm missing some React logic for this.
I use a targetContainer div with in it, several pages (fullscreen) and a Navbuttons class to move it around.
In my code below i use a 'NavButtons' functional component that sets the targetContainers 'left' value.
But when I reload the page with F5, my page stays on set style (e.g. left:-300%) but pageCounter goes back to 0, breaking the nav buttons...
I'm pretty certain its because i'm using the css-style but what's the right/best way to solve this?
import React, { useEffect, useState } from 'react';
const NavButtons = (props) => {
const maxCount = props.maxCount;
const [pageCounter, setPageCounter] = useState(0);
const scrollPrev = function () {
if (pageCounter > 0) {
setPageCounter(pageCounter - 1);
}
}
const scrollNext = function () {
if (pageCounter < (maxCount - 1)) {
setPageCounter(pageCounter + 1);
}
}
useEffect(() => {
props.targetContainer.current.style.left = -((pageCounter) * 100) + 'vw';
}, [pageCounter, props.targetContainer]);
useEffect(() => {
setToZero();
}, []);
const setToZero = function () {
setPageCounter(0);
props.targetContainer.current.style.left = 0;
}
return (
<div className="NavButtons">
<button onClick={scrollPrev}>Prev</button>
<button onClick={scrollNext}>Next</button>
</div>
)
}
export default NavButtons;
Here is a stackblitz,
https://react-zyvu7o.stackblitz.io/
Edit on:
https://stackblitz.com/edit/react-zyvu7o?file=src/components/Navbuttons.js
It 'unfortunately' works normal on stackblits, but not on my localhost... :(
I'm fairly confident this only occurs due to browser caching & hot reloading, which is why it's working in your example and not locally.
I've been struggling with this issue lately.
I'm not sure if it has any connection to "sync/async" functions in JS. If it does, I would be more then thankful to understand the connection.
I've been making a simple component function:
There's a button "next","back" and "reset". Once pressing the matching button, it allows moving between linkes, according to button's type.
The links are an array:
const links = ["/", "/home", "/game"];
Here is the component:
function doSomething() {
const [activeLink, setActiveLink] = React.useState(0);
const links = ["/", "/home", "/game"];
const handleNext = () => {
setActiveLink((prevActiveLink) => prevActiveLink+ 1);
};
const handleBack = () => {
setActiveLink((prevActiveLink) => prevActiveLink- 1);
};
const handleReset = () => {
setActiveLink(0);
};
return (
<div>
<button onClick={handleReset}>
<Link className = 'text-link' to = {links[activeLink]}> Reset</Link>
</button>
<button onClick={handleBack}>
<Link className = 'text-link' to = {links[activeLink]}>Back</Link>
</button>
<button onClick={handleNext}>
<Link className = 'text-link' to = {links[activeLink]}>Next</Link>
</button>
</div>
When I'm trying to put the activeLink in the "to" attribute of Link, it puts the old value of it. I mean, handleNext/ handleReset/handleBack happens after the link is already set; The first press on "next" needs to bring me to the first index of the links array, but it stayes on "0".
Is it has to do something with the fact that setActiveLink from useState is sync function? or something to do with the Link?
I would like to know what is the problem, and how to solve it.
Thank you.
Your Links seem to be navigating to a new page?
If so, the React.useState(0) gets called each time, leaving you with the default value of 0.
Also your functions handleNext and handleBack aren't called from what I can see.