setInterval with setState in React - javascript

I have a timer using setInterval() in a React component and I'm unsure what the best practices are in order to start and stop this interval in respect to using state. I'm running into some asynchronous issues with that.
Let's say I have a set of links in my React component that render and execute the callback fine:
let links = [10, 50, 100, 500, 1000].map((num) => {
return(
<Link key={num} onClick={(e) => this.switchNums(num)} to={`/somePath/${num}`}>{num}</Link>
)
})
Here's the switchNums() function, where i want it to reset an existing timer:
switchNums(num){
this.stopTimer()
this.reset(num)
}
Here's startTimer(), stopTimer() and reset():
startTimer(){
if(!this.state.timerId){
let timerId = setInterval(()=>{
let timer = this.state.timer + 1
this.setState({
timer: timer,
timerId: timerId
})
}, 1000)
}
}
stopTimer(){
clearInterval(this.state.timerId)
this.setState({timerId:null})
}
reset(size){
this.setState({
gameOver: false,
counter: 0,
correct: 0,
numbers: this.getRandomNumbers(size),
timer: 0
}, this.startTimer())
}
One of the bugs is clicking on the links rapidly will cause multiple intervals to fire despite the if condition in startTimer(). I'm guessing this has to do with the asynchronous nature of setState(). Another bug (and I think related) is that when i click slowly, it only starts the interval every other time.
Can anyone shed some light on this? Or what they've done to circumvent asynchronous issues with setState being used in conjunction with setInterval(any way set state can return a promise?), Or which lifecycle methods would be best for this type of situation?

I think the biggest flaw here is that you're using state to store your interval. While technically possible, I see no reason why you would actually want to do that.
Instead, just use a local variable to your component:
startTimer(){
if(!this.timerId){
this.timerId = setInterval(()=>{
//your function
}, 1000);
}
}
stopTimer(){
clearInterval(this.timerId);
}
So I don't think you need to use the state at all here for your timer. You have some other general questions in your post though that are related to state, and I'll try to answer those below. Just bear in mind that they are irrelevant in solving your particular issue.
What have they've done to circumvent asynchronous issues with setState()?
You can use a callback to execute code after the state has been set. There's a section of the official docs about this; here's what it says:
The second parameter is an optional callback function that will be executed once setState is completed and the component is re-rendered.
setState(nextState, callback);
Which lifecycle methods would be best for this type of situation?
The same section of the doc as above continues:
Generally we recommend using componentDidUpdate() for such logic instead.
If you have multiple setState in your function, and you want to execute specific code after a specific event, I think you're fine using the callback. For more general purposes use the life-cycle method above.

Using React Hooks useState and useEffect you can do the following:
const [timer, setTimer] = useState(1);
useEffect(() => {
const timerId = setInterval(() => setTimer(timer + 1), 1000);
return () => clearInterval(timerId);
});

Related

React setInterval function keeps running in the background after rerender

I am using setInterval inside useEffect with an empty dependency array inside one of my react components.
There is true/false useState which controls the display of that component.
When the state is false, the component is hidden and when it's true the component is shown.
Something like this:
const [state, setState] = useState(false)
// in jsx render section
return (
<div>
{state? <component/> : '' }
</div>
)
When the component loads for the first time the setInterval runs only one time which is what it supposes to do.
If the state goes to false then back to true, the component is removed from the UI and is then displayed back again. However, what happens here is now I have two setInterval functions running in the background, and the first one doesn't shutdown.
The number of the setInterval functions keeps increasing with each time that component re-render.
I don't understand how React works in this situation.
I need to know how it works (i.e. why won't the function shutdown, why are the number of functions increasing) and how to fix it.
This is the structure of React useEffect.React performs the cleanup when the component unmounts.
useEffect(() => {
//effect
return () => {
//cleanup runs on unmount
}
}, [])
The cleanup function should have clearInterval() which will basically removes setInterval or stops it when component unmounts. See the practical code below:
let intervalid;
useEffect(
() => {
intervalid = setInterval(() => {console.log("Iterate");}, 1000));
return function cleanup() {
console.log("cleaning up");
clearInterval(intervalid);
};
},
[]
);
This above code is just for understanding approach. Every effect may return a function that cleans up after it. This lets us keep the logic for adding and removing subscriptions close to each other. # FROM REACT DOCS Reference

Change text every 3 seconds React useEffect

I'm trying to change my text every 3 seconds using useEffect() and setInterval(). Right now it only changes the text ONE time then it doesn't change it anymore.
What am i doing wrong?
EDIT: I'm using the library "react-spring"
const [toggle, setToggle] = useState(false)
useEffect(() => {
setInterval(() => {
switch (toggle) {
case false: setToggle(true)
break;
case true: setToggle(false)
break;
}
}, 3000);
}, [])
{transitions.map(({ item, props }) => item
? <animated.div style={props}>Text that changes #1</animated.div>
: <animated.div style={props}>Text that changes #2</animated.div>)}
Solution:
useEffect(() => {
const intervalID = setTimeout(() => {
setToggle((toggle) => !toggle)
}, 3000);
return () => clearInterval(intervalID);
}, []);
Important points:
The dependency array([]) should be empty. This way you ensure that you're gonna execute this effect only on the initial mounting of the component. You need only a single interval and its creation and destruction are not dependent on a single variable but the component itself. If you put toggle in the dependency array, you will run this effect every time the variable changes thus effectively making YET ANOTHER interval on every 3 seconds. If you did supply a clean-up function, this would still work but it would be more like a setTimeout. However, in your case(without a clean-up function), this will simply introduce infinite number of intervals which will compete with each other.
You have to supply an updater function to setToggle instead of a simple value. This ensures that you're using the most current state for the update and not the stale one in the closure. If you simply provide a value, the interval is making a closure over your initial value and thus updating it. In this way, you will always update the initial false to true and this will repeat forever, leaving you with a "constant" true value.
As you're using an interval, you should provide a clean-up function to the useEffect to clear the interval on component dismount. This is very important as skipping this part will introduce memory leaks and also bugs as you'll try to update a component even after its dismount from the DOM.
Try this way
useEffect(() => {
setTimeout(() => setToggle((prevToggle) => !prevToggle), 3000);
}, [toggle]);

React - in a functional component, using hooks, can I execute a piece of code once after one setState() successfully changes state?

setState updates state asynchronously. It's my understanding that, when using a class component, you can do something like this to ensure certain code is executed after one setState updates state:
setState({color: red}, callbackThatExecutesAfterStateIsChanged);
I'm using a functional component & hooks. I'm aware, here, useEffect()'s callback will execute everytime after color state changes and on initial execution.
useEffect(callback, [color]);
How can I replicate similar behaviour as the class component example - that is, to execute a chunk of code once after one setState() successfully changes state and not on initial execution?
If you ask me, there is no safe way to do this with hooks.
The problem is that you both have to read and set an initialized state in order to ignore the first update:
const takeFirstUpdate = (callback, deps) => {
const [initialized, setInitialized] = useState(false);
const [wasTriggered, setWasTriggered] = useState(false);
useEffect(() => {
if (!initialized) {
setInitialized(true);
return;
}
if (wasTriggered) {
return;
}
callback();
setWasTriggered(true);
}, [initialized, wasTriggered]);
};
While the hook looks like it works, it will trigger itself again by calling setInitialized(true) in the beginning, thus also triggering the callback.
You could remove the initialized value from the deps array and the hook would work for now - however this would cause an exhaustive-deps linting error. The hook might break in the future as it is not an "official" usage of the hooks api, e.g. with updates on the concurrent rendering feature that the React team is working on.
The solution below feels hacky. If there's no better alternative, I'm tempted to refactor my component into a class component to make use of the easy way class components allow you to execute code once state has been updated.
Anyway, my current solution is:
The useRef(arg) hook returns an object who's .current property is set to the value of arg. This object persists throughout the React lifecycle. (Docs). With this, we can record how many times the useEffect's callback has executed and use this info to stop code inside the callback from executing on initial execution and for a second time. For example:
initialExecution = useRef(true);
[color, setColor] = useState("red");
useEffect(() => {
setColor("blue");
});
useEffect(() => {
if (initialExecution.current) {
initialExecution.current = false;
return;
}
//code that executes when color is updated.
}, [color]);

what is the logic of using an array to keep track of multiple setTimeouts

I was just going through the code of react-slick and came across the following peice of code:-
this.callbackTimers.push(
setTimeout(() => this.setState({ animating }), 10)
);
In the componentWillUnmount the callbackTimers are cleared like so :-
componentWillUnmount = () => {
if (this.callbackTimers.length) {
this.callbackTimers.forEach(timer => clearTimeout(timer));
this.callbackTimers = [];
}
};
Is the sole purpose of using the array to free memory or is there something that i have missed here ?
Why not just call the setTimeout directly:
setTimeout(() => this.setState({ animating }), 10)
instead of using an array ? i do see callbackTimers being used elsewhere too , but i don't know why exactly this array is needed apart from freeing memory, Is there any other purpose to this array ?
The line in question can be found HERE.
clearTimeout() function is only necessary to cancel any pending timers. Otherwise, you don't really need to call clearTimeout() function to clear the memory.
In the code in your question, timers are cleared in the componentWillUnmount lifecycle method to cancel any pending timers when component is about to unmount. This will prevent any code in the callback function of any pending timer, from executing once the component has unmounted.

React hooks useEffect running even with [] given as deps

I am facing quite a weird issue. Below is the code
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(count => count + 1), 1000);
return () => {
clearInterval(id);
};
}, []);
return <div>{count}</div>;
};
As per my knowledge, since I have given an empty array, the useEffect will only run after the first render. Also, since I am clearing the interval COUNT MUST NOT BE UPDATED AFTER COUNT=1. Still the setInterval seems to be running continuously. Can anyone please explain it?
Is it so that since I am given [] as deps the interval is somehow not being cleared?
clearInterval(id) - this will get invoked just before component gets unmounted. As long as the component is not unmounted the function returned from useEffect will not invoked. So the interval in not cleared and the state will continue updating thus the count increases. The interval gets cleared when the component Counter gets unmounted
This is why React also cleans up effects from the previous render before running the effects next time.
Yes, React will clean up before running the effects next time, not right after running the current effects.
Here's a brief explanation.
In your case, clearInterval(id) will only be executed when the Counter component is unmounted.
edited:
I have created a sandbox example to show when the effect is executed. Based on this example I updated my original "graph" because it looks react run the "clean effect" after rerendering.
The effect is only setting up the interval callback to update your state. setInterval will continue to run on the interval, use setTimeout instead to only run the state update once. Still return a clearTimeout in case the component unmounts within the timeout period.
Edit: More Detailed Explanation
By using an empty dependency array you are telling react that running your effect isn't dependent on any external values. The effect will run once when the component mounts (setting up the setInterval) and never run again since it isn't dependent on anything. When the component is unmounting react will run all the returned effect "cleanup" functions, i.e. the clearInterval call.
What this leaves you with is this:
Component mounts
Effect runs: sets up interval, returns cleanup function
interval is running
update count after 1000ms
update count after 1000ms
update count after 1000ms
update count after 1000ms
Component unmounts: react runs effect cleanup function, clears interval
Suggested Fix: Use setTimeout to only update state once
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setTimeout(() => setCount(count => count + 1), 1000);
return () => {
clearTimeout(id);
};
}, []);
return <div>{count}</div>;
};

Categories