I just started learning react and I have about a performance question. I want to increment a counter using the setInterval function and then reset it when the length of the array is reached. The problem is I need to add an active variable for the useEffect dependency, which removes the interval and then creates it again.
useEffect(() => {
const timer = setInterval(() => {
active == arr.length - 1 ? setActive(0) : setActive((active) => active + 1)
}, 3000)
return () => clearInterval(timer)
}, [active])
So, I wrote code like this, which looks crazy, but does the same job without removing the interval, and gets the actual version of the active variable from the callback.
useEffect(() => {
const timer = setInterval(() => {
setActive((active) => (active === arr.length - 1 ? active == 0 : active + 1))
}, 3000)
return () => clearInterval(timer)
}, [])
The question is how best to write and not do unnecessary work in the component
I would probably split all this logic out into a couple effects.
One to manage incrementing active on the interval and clearing the interval when the component unmounts.
useEffect(() => {
const timer = setInterval(() => {
setActive(active => active + 1);
}, 3000);
return () => clearInterval(timer);
}, []);
A second to reset the state on the condition.
useEffect(() => {
if (active === arr.length - 1) {
setActive(0);
}
}, [active, arr]);
I would also caution you to protect against the edge case where active is updated to 0 and the arr array is an array of length 1 as this will trigger render looping.
Related
I am trying to build a component that auto scrolls through some list, forwards and then backwards.
const [clicks, setClicks] = useState(numOfClicks)
const [goingUp, setGoingUp] = useState(true)
const scroll = () => {
if (goingUp) {
elementsRef.current[current + 1].current.scrollIntoView();
setCurrent(current + 1);
setClicks(clicks - 1);
} else {
elementsRef.current[current - 1].current.scrollIntoView();
setCurrent(current - 1);
setClicks(clicks - 1);
}
}
if clicks reaches 0 the list is at its end and then it flips around and goes backwards until it reaches the start and then the cycle repeats.
useEffect(() => {
if (clicks === 0) {
setGoingUp(!goingUp);
setClicks(numOfClicks);
}
}, [clicks])
up to this point, it works fine via click events. My last phase is to execute scroll automatically within the component after an interval. For this I use an additional useEffect
useEffect(() => {
const interval = setInterval(() => {
scroll()
}, TIMER);
return () => clearInterval(interval); // This represents the unmount function, in which you need to clear your interval to prevent memory leaks.
}, [])
The function runs once and then never again. Although if I add a console.log within the effect I see the interval is in fact working.
clearInterval is causing the setInterval to stop after 3 sec , it only run once , put your clearInterval inside a condition which will specify when you want this to stop
useEffect(() => {
const interval = setInterval(() => {
scroll()
}, TIMER);
return () => clearInterval(interval);
}, [clicks])
I want to have a countdown timer that counts from 60 to zero when the stage of program reaches two. Here's my code:
const [timer, setTimer] = useState(60)
useEffect(() => {
if (stage === 2) {
const interval = setInterval(() => {
console.log(timer)
setTimer(timer - 1)
if (timer === 1) {
clearInterval(interval)
}
}, 1000)
}
}, [stage])
and i have a div like below that just shows the counter value
<div>{timer}</div>
in my setInterval, when i use console.log(timer) it always prints out 60. But inside the div, the value starts with 60 and in the next second it will always be 59.
What am i doing wrong here?
You have closure on time === 60 value, use functional update instead.
For the same reason, you should have another useEffect for canceling the interval:
const intervalId = useRef();
useEffect(() => {
if (stage === 2) {
intervalId.current = setInterval(() => {
setTimer((prevTimer) => prevTimer - 1);
}, 1000);
}
}, [stage]);
useEffect(() => {
console.log(timer);
if (timer === 1) {
clearInterval(intervalId.current);
}
}, [timer]);
Check similar question: setInterval counter common mistakes.
I have the following code
import * as React from "react";
import { useState, useEffect } from "react";
const TxContainer: React.FunctionComponent = (props) => {
const [tx, setTx] = useState<Array<string>>([]);
useEffect(() => {
const interval = setInterval(() => {
setTx((oldArr) => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
return () => clearInterval(interval);
}, [tx.length < 10]); //this useEffect still keeps pushing even if the array is bigger than 9
useEffect(() => {
console.log(tx.length);
}, [tx.length]);
let listTx = tx.map((data, index) => (
<p key={index}>
{index} {data}
</p>
));
return <React.Fragment>{listTx}</React.Fragment>;
};
export default TxContainer;
Im trying to create random strings and put them in an array until this array is 10. Whenever, its 10 it should start deleting the first element of the array to keep working and displaying new data, but thats another story.
The point is that its not stopping when it should.
When you are passing a compare like that in the dependency array of the useEffect it will only invoke it again, when the resulting value changes. Basically those dependencies mean "if this value changes in any way, call the useEffect". Besides that it's being called on mount and on unmount. So for your code to work as you intended you should pass to the dependency array a variable that your calculations depend on and pass the logic inside the useEffet.
Here's an example of the code that you described, of course I don't know if you want to use the same timeout or not.
useEffect(() => {
let interval;
if (tx.length < 5) {
interval = setInterval(() => {
setTx(oldArr => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
} else {
interval = setInterval(() => {
setTx(oldArr => [...oldArr.slice(1)]);
}, Math.floor(Math.random() * 3000) + 1000);
}
return () => clearInterval(interval);
}, [tx.length ]);`
Here is the solution: second useEffect is not needed. This effect runs first time and whenever array length is changed. when it reaches 11, clear the interval or remove first element.
useEffect(() => {
const interval = setInterval(() => {
setTx((oldArr) => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
if(tx.length === 11){
setTx(prev=>prev.slice(1))
// clearInterval(interval)
}
return () => clearInterval(interval);
}, [tx.length]);
I'm trying to change the color of a SVG structure from red to white and white to red using React Hooks. The problem is that after a few seconds the color starts changing rapidly instead of every second. I'm not understanding where I'm going wrong in the code.
Here is the useState.
const[rectColor, rectColorSet] = useState('red')
And the useEffect for changing the color.
useEffect(() => {
if(rectColor === 'red'){
let timer = setInterval(() => {
rectColorSet('white')
}, 1000)
}
// console.log('timer',timer)
else if(rectColor ==='white'){
let timer = setInterval(() => {
rectColorSet('red')
}, 1000)
}
})
And this is the place where I use the state which contains color.
d={`
M-0.20 0 L-0.20 0.30 L0.20 0.30 L0.20 0 L-0.20 0
Z`}
fill={props.value ? "green" : rectColor}
/>
}
Every render's setting a new interval. Use setTimeout instead of setInterval, and don't forget to clean up the effects:
useEffect(() => {
if (rectColor === 'red' || rectColor === 'white') {
const timeoutID = setTimeout(() => {
rectColorSet(rectColor === 'red' ? 'white' : 'red');
}, 1000);
return () => clearTimeout(timeoutID);
}
});
Or toggle between a boolean instead of using a string for state. If you add other state that might change, using a single interval when the component mounts might be easier to manage:
const [red, setRed] = useState(true);
useEffect(() => {
const intervalID = setInterval(() => {
setRed(red => !red);
}, 1000);
return () => clearInterval(intervalID);
}, []);
After a few seconds, the color starts changing rapidly instead of every second because you put [] in useEffect.
If you want to run an useEffect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as the second argument in useEffect hook.
And You should use setTimeout() intend of setInterval()
So you should write your useEffect like this:
const [red, setRed] = useState(true);
useEffect(() => {
const intervalID = setTimeout(() => {
setRed(red => !red);
}, 1000);
return () => clearInterval(intervalID);
}, []);
You actually need to replace setInterval() with setTimeout()
and rather than declaring everthing seperately use OR in the conditional and clean up the code.
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>;
}