Reputation: 1391
I'm currently building a timer in ReactJS for practice. Currently I have two components, a Timer
component which displays the time as well as sets an interval upon mounting. I also have an App
component which keeps tracks of the main state, such as state for whether the timer is paused
as well as the current value of the timer
.
My goal is to make it so that when I click the pause
button, the timer stops incrementing. Currently I've tried to achieve this by using:
if(!paused)
tick(time => time+1);
which in my mind, should only increment the time
state when paused
is false
. However, when I update the paused
state by clicking on my button, this paused
state inside the setTimeout does not change. My guess that setTimeout is forming a closure over the paused
state, so it's not updating when the state changes. I tried adding paused
as a dependency to useEffect
but this caused multiple timeouts to be queued whenever paused
changed.
const {useState, useEffect} = React;
const Timer = ({time, paused, tick}) => {
useEffect(() => {
const timer = setInterval(() => {
if(!paused) // `paused` doesn't change?
tick(time => time+1);
}, 1000);
}, []);
return <p>{time}s</p>
}
const App = () => {
const [time, setTime] = useState(0);
const [paused, setPaused] = useState(false);
const togglePaused = () => setPaused(paused => !paused);
return (
<div>
<Timer paused={paused} time={time} tick={setTime} />
<button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
So, my question is:
paused
not updating within useEffect
)?useEffect()
?Upvotes: 1
Views: 2230
Reputation: 1534
To stop the timer return the function clearing the interval from useEffect()
.
React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.
You should also pass the paused
in dependencies array to stop useEffect
creating new intervals on each re-render. If you add [paused]
it'll only create new interval when paused
change.
const {useState, useEffect} = React;
const Timer = ({time, paused, tick}) => {
useEffect(() => {
const timer = setInterval(() => {
if(!paused) // `paused` doesn't change?
tick(time => time+1);
}, 1000);
return () => clearInterval(timer);
}, [paused]);
return <p>{time}s</p>
}
const App = () => {
const [time, setTime] = useState(0);
const [paused, setPaused] = useState(false);
const togglePaused = () => setPaused(paused => !paused);
return (
<div>
<Timer paused={paused} time={time} tick={setTime} />
<button onClick={togglePaused}>{paused ? 'Play' : 'Pause'}</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 4
Reputation: 1316
You can use Dan's useInterval
hook. I can't explain it better than him why you should use this.
The hook looks like this.
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Then use it in your Timer
component like this.
const Timer = ({ time, paused, tick }) => {
useInterval(() => {
if (!paused) tick(time => time + 1);
}, 1000);
return <p>{time}s</p>;
};
I believe the difference is that useInterval
hook is aware of its dependencies (paused) while setInterval
isn't.
Upvotes: 2
Reputation: 406
The setInterval captures the paused
value the first time, you'll need to remove the interval, recreate it everytime paused is changed.
You can check this article for more info: https://overreacted.io/making-setinterval-declarative-with-react-hooks
Upvotes: 2