useEffect(() => {
const id_1 = setTimeout(() => {
// do something
clearTimeout(id_1);
}, 500);
}, [success_failure_msg[0]]);
vs
useEffect(() => {
const id_1 = setTimeout(() => {
// do something
}, 500);
return () => clearTimeout(id_1);
}, [success_failure_msg[0]]);
What is the difference and which is the correct practice?
The correct practice is the second one that you provided. To answer your question you really have to understand why we clear the timeout.
Let's take an example of a simple react component:
const Comp = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount(1); // set the count in the timeout
}, 1000);
}, [setCount]);
return (
<div>count: {count}</div>
);
}
Now this will work as long as the component is rendered. But what happens if the component gets unmounted before the timeout is resolved. The timeout will still run and you will be calling setCount of a component that is no longer in the view.
If you change it to the code you have given in your first example the problem is still the same:
useEffect(() => {
const id_1 = setTimeout(() => {
clearTimeout(id1); // this does not do anything as the timeout has already run
setCount(1); // set the count in the timeout
}, 1000);
}, [setCount]);
The clearing the timeout does not do anything and you will still have a memory leak. The only way to resolve it is to return a function from the useEffect that react will run when it unmounts the component. Within that function you can clean up the code.
useEffect(() => {
const id_1 = setTimeout(() => {
setCount(1); // set the count in the timeout
}, 1000);
// React will run this function when it unmounts and so no memory leak
return () => clearTimeout(id_1)
}, [setCount]);
both seems wrong to me, cleartimeout should be before settimetimeout
and when success_failure_msg changes two time the settimeout trigger only once.
Example
var id_1 = undefined;
useEffect(() => {
clearTimeout(id_1)
id_1 = setTimeout(() => {
success_failure_msg // shoule be test2
}, 500);
}, [success_failure_msg]);
success_failure_msg = "test";
success_failure_msg = "test2"
Related
I took code based off this page and adjusted it. I want to time the amount of milliseconds the user is on the component so I want to log the counter value when the component unmounts aka the return statement of useffect/componentWillUnmount().
const [milliseconds, setMilliseconds] = useState(0);
const isActive = useState(true);
const logger = new logger(stuff);
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
});
useEffect(() => {
return () => {
console.log("empty useffect milliseconds:", milliseconds);
logger(milliseconds);
clearInterval(milliseconds)
};
}, []);
useEffect(() => {
let interval: NodeJS.Timeout = setInterval(() => {
}, 0);
interval = setInterval(() => {
setMilliseconds(milliseconds => milliseconds + 1000);
}, 1000);
console.log("interval:", interval);
console.log("interval milliseconds:", milliseconds);
}, [ milliseconds]);
I see the millisecond printout fine in the "interval milliseconds" console statement but the "empty useffect milliseconds:" always prints out 0. What am I doing wrong?
You can remember a mount timestamp and then calculate the difference.
useEffect(() => {
const mountedAt = Date.now();
return () => {
const mountedMilliseconds = Date.now() - mountedAt;
console.log(mountedMilliseconds);
};
}, []);
Side note 1: use an empty array as deps if you want to run function on mount only. If you do not pass [] deps, your initializeIcons effect will run with each re-render. Do it like this:
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
}, []);
Side note 2: first interval you create creates a memory leak, because it does nothing, and is never cleared.
Another problem you have is milliseconds dependency in useEffect, which registers new intervals after each milliseconds state change.
I'm currently trying to create a stopwatch component using React Native and the setInterval function to increment a counter and set the new value to state like so:
Play Function(this runs after hitting the play button)
const [isRunning, setisRunning] = useRecoilState(runState);
const [time, setTime] = useRecoilState(timeState);
const [timerRef, setTimerRef] = useState(0);
const Play = useCallback(() => {
window.clearInterval(interval); //clear the interval
if (isRunning === false) { //set the isRunning state to true
setisRunning((isRunning) => !isRunning);
interval = window.setInterval(() => { //setInterval operation
setTime((previousTime) => previousTime + 1);
}, 100);
setTimerRef(interval); //setTimer reference to pause
}
}, [time]);
Pause Function(this runs after hitting the pause button)
const Pause = () => {
if (isRunning === true) {
setisRunning((isRunning) => !isRunning);
window.clearInterval(timerRef);
}
};
My problem is that the timer tends to speed up erratically an as such I'm not able to use the time value passed. I'm not sure where I am going wrong, kindly assist.
Ok so Iv've run into this quite recently in my next.js project. The correct way to do it seems like something like that:
useEffect(() => {
const interval = setInterval(() => {
console.log('This will run every second!');
}, 1000);
return () => clearInterval(interval);
}, []);
See for example here
I'm loading multiple animals into my ThreeJS project. All these animals have PositionalAudio with a setInterval function. I use them inside a useEffect function. On the callback I want to clear the interval, but it keeps calling the function.
This is the function where I set my setInterval:
const loadAudio = () => {
const animalSound = new THREE.PositionalAudio(listener);
animalSound.setBuffer(animalBuffer);
playSounds = setInterval(() => {
animalSound.play();
} , 5000);
audios.push(animalSound);
}
In the return function I try to clear the interval:
return () => {
audios.forEach((audio) => {
audio.stop();
clearInterval(playSounds);
});
};
Sadly the audio keeps playing every 5 seconds
Here is a code snippet
https://codesandbox.io/s/bitter-tree-bb4ld?file=/src/App.js
According to your code snippet, say you have Button:
<button
onClick={buttonToggle}
>
{start ? 'start' : 'stop'}
</button>
Initially we have some setup for useState and handle click function
const [seconds, setSeconds] = useState(0);
const [btnStart, setBtnStart] = useState(true);
const buttonToggle = useCallback(
() => setBtnStart(run => !run)
, []);
In the useEffect you will do following changes
useEffect(() => {
if(!btnStart) {
// setSeconds(0); // if you want to reset it as well
return;
}
const interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, [btnStart]);
I want to have all of the setTimeOut cleared when the component is unmounted.
Even though I have use clearTimeOut as a clean up function but the error still persis: "Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function"
useEffect(() => {
const timeOut = {timeout1: ()=>setTimeout(() => setProgress((preV) => preV + 15), [
550,
]),timeout2 : ()=> setTimeout(() => setMessage("All most done"), [500])}
timeOut.timeout1();
timeOut.timeout2();
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
};
}, [progress,message]);
Does anyone know how to solve this issue? Any help would be appreciated!
timeout1 isn't storing the returned value of setTimeout which is the timerId but its storing the reference of the function that executes timeout
You could write your code in a way that it executes the timeouts immediately using Immediately invoked functions so that timeout1 and timeout2 have timerIds
useEffect(() => {
const timeOut = {
timeout1: (()=>setTimeout(() => setProgress((preV) => preV + 15), 550))(),
timeout2 : (()=> setTimeout(() => setMessage("All most done"), 500))()
}
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
};
}, [progress,message]);
however you could simply run the timeouts without writing them as IIFE
useEffect(() => {
const timeOut = {
timeout1: setTimeout(() => setProgress((preV) => preV + 15), 550),
timeout2 : ()=> setTimeout(() => setMessage("All most done"), 500)
}
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
};
}, [progress,message]);
How about to use a local variable that keeps track of whether the component is mounted or not.
useEffect(() => {
let run = true;
const timeOut = {
timeout1: () => setTimeout(() => setProgress((preV) => preV + 15), [550]),
timeout2: () => setTimeout(() => setMessage('All most done'), [500]),
};
if (run) {
timeOut.timeout1();
timeOut.timeout2();
}
return () => {
clearTimeout(timeOut.timeout1);
clearTimeout(timeOut.timeout2);
run = false;
};
}, [progress, message]);
Why does this not work as a normal one second counter?
function UseEffectBugCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}
Example: https://codesandbox.io/s/sparkling-rgb-6ebcp
-Is it because of stale closures?
or
-Is it because the count is a state variable and the component would be re-rendered after the state update so a new interval will be created creating some sort of loop?
or
-Is it something else?
I'm looking for a why this occurs in this answer if possible, there are a few different articles stating why it doesn't work (as per above). But none have been able to provide a good argument so far.
You can use callback for set state to use latest counter value:
setCount(count => (count + 1));
You may need to add the dependency for count in useEffect. Currently useEffect is only called on the mount and is not called after that (i.e when the count value changes).
So it always says 0 because useEffect is executed only once ( on mount ) and that time the count value is set to 0. And thus it every time it logs 0 on setInterval due to closure.
I have updated the code sandbox to find the reason and meaningful logs. Here is the sandbox link: https://codesandbox.io/s/admiring-thompson-uz2xe
How to find closure value: You can check the logs and traverse through prototype object to find [[Scopes]] and you will get the values as seen in the below screenshot:
This would work:
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}, [count]);
You can check this doc: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
You can read this as well: You can read this as well: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Hope this helps!
If you removed second params for useEffect your application will be rendered always if u have some change in state, but this is bad practice. You need in second params choose for wich parametrs you need watch ...
Example with choosen params:
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}[count]);
Without
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
});
Because you didn't add count to useEffect's dependences, then inside the effect count always is 0.
You should use useReducer to resolve your problem:
function UseEffectBugCounter() {
const [count, dispatchCount] = React.useReducer((state, { type }) => {
switch(type) {
case 'inc':
return state + 1
default:
return state
}
}, 0);
React.useEffect(() => {
const intervalId = setInterval(() => {
dispatchCount({ type: 'inc'})
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}
You need to pass count instead of blank array in useEffect
function UseEffectBugCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
},[count]);
return <div>The count is: {count}</div>;
}