SetInterval causes too many re-renders React - javascript

Hello I would like to put setInterval in my React project to add 1 for each second but I got an error like in title of this post.
js:
const [activeTab, setActiveTab] = useState(0)
useEffect(() => {
setInterval(setActiveTab(prevTab => {
if (prevTab === 3) return 0
console.log('hi')
return prevTab += 1
}), 1000)
})

There are a few issues:
You're not passing a function to setInterval, you're calling setActiveTab and passing its return value into setInterval.
If you were passing in a function, you'd be adding a new repeated timer every time your componennt ran. See the documentation — useEffect:
By default, effects run after every completed render...
And setInterval:
The setInterval() method... repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.
(my emphasis)
Starting a new repeating timer every time your component re-renders creates a lot of repeating timers.
Your component will leave the interval timer running if the component is unmounted. It should stop the interval timer.
To fix it, pass in a function, add a dependency array, and add a cleanup callback to stop the interval:
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
const handle = setInterval(() => { // *** A function
setActiveTab(prevTab => {
if (prevTab === 3) return 0;
console.log("?ghi");
return prevTab += 1;
});
}, 1000);
return () => { // *** Clear the interval on unmount
clearInterval(handle); // ***
}; // ***
}, []); // *** Empty dependency array = only run on mount
Side note: Assuming you don't need the console.log, that state setter callback function can be simpler by using the remainder operator:
setActiveTab(prevTab => (prevTab + 1) % 3);

useEffect(() => {
setInterval(() => {
setActiveTab((prevTab) => {
if (prevTab !== 3) {
return (prevTab += 1);
} else {
return 0;
} // console.log("hi");
});
}, 1000);
}, []);

Related

set time dynamic time for set interval in react

Hello i am struggling to set dynamic time for settimeout function in react js.
i have long string of key value pair of time and message. i wants to display each message for specific time and loop through whole list.
here is what i am trying, but not working.
const [timer, setTimer] = useState(0)
const [time, setTime] = useState(5000)// this is default value to start which need to update with str time value
const str=[{name:"rammy", time:1000},
{name:"james", time:4000},
{name:"crown", time:2000}]
useEffect(()=>{
const getTime= str[timer].time
setTime(getTime)
},[timer])
//when timer change it should update update time state which will be used to update time for time settime out
function increment() {
useEffect(()=>{
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
},[timer])
} // above code is for increment time state on each iteration
function ButtonHandle(){
//setRealString(itr)
increment()
} //button handler for start timer
First of all, you can't put hooks inside functions (other than your component functions). https://reactjs.org/docs/hooks-rules.html
So take the useEffect out of increment()
useEffect(()=>{
increment()
},[timer])
function increment() {
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
}
But you also need to clear the timeout. We can return the timeout function to reference it, and clear the time out with a return inside useEffect. Clearing timeouts and intervals in react
useEffect(()=>{
const myTimeout = increment()
return () => {
clearTimeout(myTimeout)
}
},[timer])
function increment() {
return setTimeout(() => {
setTimer((ele) => ele + 1);
}, time);
}
Then we can combine the useEffects which both have a dependancy array of [timer].
useEffect(() => {
const getTime = str[timer].time;
setTime(getTime);
const myTimeout = increment();
return () => {
clearTimeout(myTimeout);
};
}, [timer]);
You don't need to use useEffect to do it. You misunderstood the useEffect usage, it's a react hook to you implement side-effects and you can't use react-hooks inside a function, it should be in the component scope.
I can increment directly from the ButtonHandle function.
// On the index state is necessary in this implementation
const [index, setIndex] = useState(-1)
const guys=[
{name: "rammy", time:1000},
{name: "james", time:4000},
{name: "crown", time:2000}
]
// useCallback memoize the increment function so that it won't
// change the instance and you can use it in the useEffect
// dependency array
const increment = useCallback(() => {
setIndex((i) => i+1)
}, [])
useEffect(() => {
// change the state if the timer is greater than -1
if (index !== -1) {
if (index >= guys.length) {
setIndex(-1);
} else {
setTimeout(() => {
increment();
}, guys[index].time); // <-- you get the time from your array
}
}
}, [index, increment]);
function handleClick(){
//setRealString(itr)
increment()
}
Even though I helped you, I don't know what you're trying to do. This implementation sounds like a code smell. We can help you better if you explain the solution you're trying to do instead of just the peace of code.
You don't need to set the time state, as you already have the time in the array; avoiding unnecessary state changes is good.

ReactJS counter or timer with Start or Stop Button

Need help to understand in useEffect, if I don't put counter and timerCheck in useEffect dependency then what it will effect here.
And if I put timerCheck dependency in useEffect then counter increasing 100 time faster
Also how can i run this code without any error or warning
code in codesandbox
const [counter, setCounter] = useState(0);
const [startcounter, setStartcounter] = useState(false);
const [timerCheck, setTimerCheck] = useState(0);
useEffect(() => {
let intervalStart = null;
if (startcounter) {
intervalStart = setInterval(() => {
setCounter((prevState) => (prevState += 1));
}, 1000);
setTimerCheck(intervalStart);
} else {
clearInterval(timerCheck);
}
return () => {
if (counter !== 0) clearInterval(timerCheck);
};
}, [startcounter]);
const handleStartButton = () => {
setStartcounter(true);
};
const handleStopButton = () => {
setStartcounter(false);
};
Try not to declare unnecessary states. timerCheck state is redundant.
You want to start an interval that increases counter by one every second.
When user stops the stopwatch , you want the interval to be cleared, so it would stop increasing the timer.
So your only dependancy in useEffect would be whether your stopwatch is running or not. And the only thing that matters to you, is when startCounter is ture. You start an interval when it is true and clear the interval in its cleanup.
useEffect(() => {
let intervalStart = null;
if (!startcounter) {
return () => {};
}
intervalStart = setInterval(() => {
setCounter((prevState) => (prevState += 1));
}, 1000);
return () => {
clearInterval(intervalStart);
};
}, [startcounter]);
Here is the codesandbox
But you do not really need useEffect for this. IMO it would be a much better code without using useEffect.
So useEffect with no dependency will run on every render.
If you include a dependency, it will re-render and run whenever that dependency changes. So by including timerCheck in it, you're telling the page to re-render whenever that changes, which ends up changing it, causing it to re-render again, causing it to change again, etc., etc.
More info: https://www.w3schools.com/react/react_useeffect.asp

How to manually stop a watcher?

I need to stop watch() but the docs don't really explain how to do that.
This watcher runs until the loop is finished (1000 seconds):
const state = reactive({
val: 0
})
watch(() => state.val, () => {
console.log(state.val)
})
for (let i = 1; i < 1000; i++) {
setTimeout(function timer() {
state.val = state.val + 1
}, i * 1000);
}
How do I stop the watcher after running once? Using watchEffect is not an option because for my use case the watcher needs to run several times before stopped, which is not described in this simplified example. From my understanding watchEffect runs only once (after initiation).
The "watch" function returns a function which stops the watcher when it is called :
const unwatch = watch(someProperty, () => { });
unwatch(); // It will stop watching
To watch a change only once:
const unwatch = watch(someProperty, () => {
// Do what your have to do
unwatch();
});

Same logic but different behaviour in 'class' and in 'functional component'

Attempted to translate an example code from class to functional component and faced the problem.
the target file is in components/Wheel/index.js
Key function that causes problem
const selectItem = () => {
if (selectedItem === null) {
const selectedItem = Math.floor(Math.random() * items.length);
console.log(selectedItem);
setSelectedItem(selectedItem);
} else {
setSelectedItem(null);
let t= setTimeout(() => {
selectItem()
}, 500);
clearTimeout(t);
}
};
First time is normal,
from second time onward,
2 clicks are needed for the wheel to spin.
I had to add clearTimeout() or infinite loop is resulted, but the same does not happen in the original.
Original working example in class
My version in functional component.
MyVersion
Thank you.
What an excellent nuance of hooks you've discovered. When you call selectItem in the timeout, the value of selectedItem that is captured in lexical scope is the last value (not null).
There's two answers, a simple answer and a better working answer.
The simple answer is you can accomplish it be simply separating the functions: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
It looks like this:
const doSelect = () => {
setSelectedItem(Math.floor(Math.random() * items.length));
};
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
setTimeout(doSelect, 500);
}
};
Now, read on if you dare.
The complicated answer fixes the solution for the problem if items.length may change in between the time a timer is set up and it is fired:
https://codesandbox.io/s/spinning-wheel-game-forked-wmeku
Rerendering (i.e. setting state) in a timeout causes complexity - if the component re-rendered in between the timeout, then your callback could've captured "stale" props/state. So there's a lot going on here. I'll try and describe it as best I can:
const [selectedItem, setSelectedItem] = useState(null);
// we're going to use a ref to store our timer
const timer = useRef();
const { items } = props;
// this is just the callback that performs a random select
// you can see it is dependent on items.length from props
const doSelect = useCallback(() => {
setSelectedItem(Math.floor(Math.random() * items.length));
}, [items.length]);
// this is the callback to setup a timeout that we do
// after the user has clicked a "second" time.
// it is dependent on doSelect
const doTimeout = useCallback(() => {
timer.current = setTimeout(() => {
doSelect();
timer.current = null;
}, 500);
}, [doSelect]);
// Here's the tricky thing: if items.length changes in between
// the time we rerender and our timer fires, then the timer callback will have
// captured a stale value for items.length.
// The way we fix this is by using this effect.
// If items.length changes and there is a timer in progress we need to:
// 1. clear it
// 2. run it again
//
// In a perfect world we'd be capturing the amount of time remaining in the
// timer and fire it exactly (which requires another ref)
// feel free to try and implement that!
useEffect(() => {
if (!timer.current) return;
clearTimeout(timer.current);
doTimeout();
// it's safe to ignore this warning because
// we know exactly what the dependencies are here
}, [items.length, doTimeout]);
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
doTimeout();
}
};

React state is not properly updating

I am working on a Pomodoro clock and facing an issue when switching back to pomodoro session after an break.
const start = () => {
//this stop function clears the pre-existing timer if there is any.
stop();
setTimerOn(true);
let interval = setInterval(() => {
setDisplayTime((prev) => {
//onBreak is false by default
if (prev <= 0 && !onBreak) {
setOnBreak(true);
return breakDuration;
}
// this if statement is not being executed
if (prev <= 0 && onBreak) {
setOnBreak(false);
return sessionDuration;
}
return prev - 1;
});
}, 1000);
//save the timer insatance in react state
setTimer(interval);
};
I am updating the state of onBreak whenver the timer becomes 0. But when I check the state of onBreak in react dev tools then only first if block executed and after that the state never changes. The code went in infinite loop after execution of first if statement and only breakSession is being executed over and over again.
The issue with your code is that the function you pass to setInterval saves the value of onBreak when it was created; it has no way of getting the current state of onBreak. (I'm assuming [onBreak, setOnBreak] = useState().)
One way to pass in changeable state is useRef. Something like this:
const [onBreak, setOnBreak] = useState();
// Create a ref whose value is set automatically to onBreak
const onBreakRef = useRef();
useLayoutEffect(() => {
onBreakRef.current = onBreak;
}, [onBreak]);
const start = () => {
//this stop function clears the pre-existing timer if there is any.
stop();
setTimerOn(true);
let interval = setInterval(() => {
setDisplayTime((prev) => {
if (prev <= 0 && !onBreakRef.current) {
setOnBreak(true);
return breakDuration;
}
if (prev <= 0 && onBreakRef.current) {
setOnBreak(false);
return sessionDuration;
}
return prev - 1;
});
}, 1000);
//save the timer insatance in react state
setTimer(interval);
};
This code feels messy though. Could you instead do setOnBreak(false) in the other code that reacts to when onBreak is true? Then the setInterval code could unconditionally setOnBreak(true) when prev <= 0.

Categories

Resources