How to update state using setInterval on functional components in React - reactjs

I am trying to implement a countdown, but the state is not being update as expected. It stays stuck on initial value 30. I have no clue how to solve it. Can anyone help me please?
const [timer, setTimer] = useState(30);
function handleTimer() {
const interval = setInterval(() => {
setTimer((count) => count - 1);
if (timer <= 0) {
clearInterval(interval);
}
}, 1000);
}
useEffect(() => {
handleTimer();
}, []);

The problem is about javascript closures, you can read more about it here
Also, Dan has a full detailed article talking about this specific problem. I strongly suggest you read it.
And here is a quick solution and demonstration for your problem. First of all, the useEffect will be executed every time the component is remount. And this could happen in many different scenarios depending on your code. Hence, The useEffect starts fresh and closes on new data every time.
So all we need is to save our values into ref so we can make use of the same reference every re-render.
// Global Varibales
const INITIAL_TIMER = 30;
const TARGET_TIMER = 0;
// Code refactoring
const [timer, setTimer] = useState(INITIAL_TIMER);
const interval = useRef();
useEffect(() => {
function handleTimer() {
interval.current = setInterval(() => {
setTimer((count) => count - 1);
}, 1000);
}
if (timer <= TARGET_TIMER && interval.current) {
clearInterval(interval.current);
}
if (timer === INITIAL_TIMER) {
handleTimer();
}
}, [timer]);

You can find here a more generic hook to handle the setInterval efficiently in react with pause and limit the number of iterations:
https://github.com/oulfr/react-interval-hook

Related

how to unmount a function in useEffect (react js)

test function dose not unmount and wen i click on correectAnswer the last function (test) is steal running and again test function will run and then when the last test function achieve to 0 we go to loser page.
const [state, setState] = useState({
haveTime: 10
})
const [states] = useState({
correct: "question",
step: "loser"
})
const test = (timer) => {
let haveTime = 10
let time = setInterval(() => {
haveTime -= 1;
setState({ haveTime })
// console.log(state.haveTime)
}, 1000);
setTimeout(() => {
clearInterval(time)
dispatch(getNameStep(states.step))
}, timer);
}
const correectAnswer = () => {
if (index === 9) {
dispatch(getNameStep(stateForWinner.step))
}
else {
dispatch({
type: "indexIncrease"
})
test(10000)
}
}
let { question, correct_answer } = details.question[index];
useEffect(() => {
test(10000)
}, [])
There are a few things wrong with your code.
First you are combining setInterval with setTimeout which is not a good idea just because of the amount of coordination that needs to happen.
Second to clear an interval or a timeout you need to do it from within the useEffect by returning a function.
Third you have no "dependencies" in your useEffect.
Look at this code that use in one my apps:
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
SetSearchFilter(SearchLocal, State.Reversed);
}, 250)
// this is how you clear a timeout from within a use effect
// by returning a function that does the disposing
return () => clearTimeout(delayDebounceFn);
}, [SearchLocal]);//here you need to add the actual dependencies of your useEffect
Lastly you need to breakdown your useEffect to perform a "single effect". Combining "too much stuff" into a single use effect is not good because then it is very difficult to debug and to achieve what you want.
You need to break down your useEffect into smaller useEffects.
You need to tell the useEffect when you want it to run by adding the dependencies. This way you know that a particular useEffect will run for ecxample if the "nextStep" has changed or if the test has reached the end.

React: Why do we need to utilize useEffect for timers?

According to the React docs as well as every example on stackoverflow for timers, people use something similar to Option 2 w/ useEffect (+useState) to create a timer that you can start/pause/reset.
However, I've also been able to create a timer in Option 1 by solely using useState.
Why does nobody rely on useState for timers? I understand that useEffect cleans up during unmounting/re-rendering, but does this really improve performance? Wouldn't the constant unmounting and remounting from useEffect and then calling setValue be slower than just executing a regular function that calls setValue? Both options can call clearInterval, so shouldn't either be sufficient for clean-up?
Also, which timer would be more "accurate", Option 1 or 2? I believe I understand how the Event Loop for async functions works, but in React it becomes a bit foggy to me. Would there ever be a case where multiple async functions are backlogged and somehow delay useEffect from triggering and making the timer in Option 2 tick at a slower rate than Option 1 (i.e. not ticking exactly every second and slowly lagging behind the other timer)?.
Thank you!
Option 1 - regular function + useState
const [time, setTime] = useState(1500);
const [startPauseBtnText, setStartPauseBtnText] = useState('START');
const timeID = useRef(null);
const startPauseTime = () => {
if (timeID.current) {
clearInterval(timeID.current);
timeID.current = null;
setStartPauseBtnText('START');
} else {
timeID.current = setInterval(() => {
setTime((prevTime) => {
return prevTime - 1;
});
}, 1000);
setStartPauseBtnText('PAUSE');
}
};
const resetTime = () => {
clearInterval(timeID.current);
timeID.current = null;
setTime(1500);
};
Option 2 - useEffect + useState
const [isActive, setIsActive] = useState(false);
const [time2, setTime2] = useState(1500);
useEffect(() => {
let timeID2;
if (isActive) {
timeID2 = setInterval(() => {
setTime2((prevTime) => {
return prevTime - 1;
});
}, 1000);
}
return () => clearInterval(timeID2);
}, [isActive, time2]);
const resetTime2 = () => {
setIsActive(false);
setTime2(1500);
};
useEffect allows you to clear the timer when your component unmounts (via the returned cleanup function) instead of leaving it running and triggering a future state update attempt for a component that isn’t there anymore.

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

useEffect hook using outdated variables in child functions

I am trying to use the useEffect hook as a way to create an async timer in react. The logic is inside of timeFunc(), and the useEffect is working such that it calls the function every 1000ms. The weird part is, for some reason when timeFunc() gets called (every one sec) it's only accesses the old variable values, (specifically "paused"). For example, if the interval starts with a value of "paused" being false, even if I change 'paused' to be true (paused is a state variable passed in by the parent component), timeFunc() will still think paused is false. Can't figure it out. Any help appreciated!
Code:
//TIMER MANAGER
let timeFunc = () => {
if(paused == false){
let delta = Math.trunc((new Date() - resumedTime)/1000);
setProgress(delta);
console.log('test + ' + paused);
} else {
clearInterval(interval);
}
}
useEffect(() => {
let interval = null;
interval = setInterval(() => {
timeFunc();
}, 1000);
return () => clearInterval(interval);
}, [initialized]);
The timeFunc depends on having an up-to-date value of paused, but it doesn't exist in the useEffect's dependency array.
Either add it to the dependency array and also store the time until the next interval in state, or use a ref for paused instead (with a stable reference) (or in addition to state), eg:
const pausedRef = useRef(false);
// ...
const timeFunc = () => {
if (!pausedRef.current) {
// ...
// to change it:
pausedRef.current = !pausedRef.current;
Also note that
let interval = null;
interval = setInterval(() => {
timeFunc();
}, 1000);
simplifies to
const interval = setInterval(timeFunc, 1000);

react hooks and setInterval

Is there any alternative to just keeping a "clock" in the background to implement auto-next (after a few seconds) in carousel using react hooks?
The custom react hook below implements a state for a carousel that supports manual (next, prev, reset) and automatic (start, stop) methods for changing the carousel's current (active) index.
const useCarousel = (items = []) => {
const [current, setCurrent] = useState(
items && items.length > 0 ? 0 : undefined
);
const [auto, setAuto] = useState(false);
const next = () => setCurrent((current + 1) % items.length);
const prev = () => setCurrent(current ? current - 1 : items.length - 1);
const reset = () => setCurrent(0);
const start = _ => setAuto(true);
const stop = _ => setAuto(false);
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, 3000);
return _ => clearInterval(interval);
});
return {
current,
next,
prev,
reset,
start,
stop
};
};
There are differences between setInterval and setTimeout that you may not want to lose by always restarting your timer when the component re-renders. This fiddle shows the difference in drift between the two when other code is also running. (On older browsers/machines—like from when I originally answered this question—you don't even need to simulate a large calculation to see a significant drift begin to occur after only a few seconds.)
Referring now to your answer, Marco, the use of setInterval is totally lost because effects without conditions dispose and re-run every time the component re-renders. So in your first example, the use of the current dependency causes that effect to dispose and re-run every time the current changes (every time the interval runs). The second one does the same thing, but actually every time any state changes (causing a re-render), which could lead to some unexpected behavior. The only reason that one works is because next() causes a state change.
Considering the fact that you are probably not concerned with exact timing, is is cleanest to use setTimeout in a simple fashion, using the current and auto vars as dependencies. So to re-state part of your answer, do this:
useEffect(
() => {
if (!auto) return;
const interval = setTimeout(_ => {
next();
}, autoInterval);
return _ => clearTimeout(interval);
},
[auto, current]
);
Generically, for those just reading this answer and want a way to do a simple timer, here is a version that doesn't take into account the OP's original code, nor their need for a way to start and stop the timer independently:
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
// You could also do `setCounter((count) => count + 1)` instead.
// If you did that, then you wouldn't need the dependency
// array argument to this `useEffect` call.
}, 1000);
return () => {
clearTimeout(id);
};
},
[counter],
);
However, you may be wondering how to use a more exact interval, given the fact that setTimeout can drift more than setInterval. Here is one method, again, generic without using the OP's code:
// Using refs:
const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
() => {
const id = setInterval(() => {
r.current.setCounter(r.current.counter + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
// Using the function version of `setCounter` is cleaner:
const [counter, setCounter] = useState(30);
useEffect(
() => {
const id = setInterval(() => {
setCounter((count) => count + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
Here is what is going on above:
(first example, using refs): To get setInterval's callback to always refer to the currently acceptable version of setCounter we need some mutable state. React gives us this with useRef. The useRef function will return an object that has a current property. We can then set that property (which will happen every time the component re-renders) to the current versions of counter and setCounter.
(second example, using functional setCounter): Same idea as the first, except that when we use the function version of setCounter, we will have access to the current version of the count as the first argument to the function. No need to use a ref to keep things up to date.
(both examples, continued): Then, to keep the interval from being disposed of on each render, we add an empty dependency array as the second argument to useEffect. The interval will still be cleared when the component is unmounted.
Note: I used to like using ["once"] as my dependency array to indicate that I am forcing this effect to be set up only once. It was nice for readability at the time, but I no longer use it for two reasons. First, hooks are more widely understood these days and we have seen the empty array all over the place. Second, it clashes with the very popular "rule of hooks" linter which is quite strict about what goes in the dependency array.
So applying what we know to the OP's original question, you could use setInterval for a less-likely-to-drift slideshow like this:
// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...
const r = useRef(null);
r.current = { next };
useEffect(
() => {
if (!auto) return;
const id = setInterval(() => {
r.current.next();
}, autoInterval);
return () => {
clearInterval(id);
};
},
[auto]
);
Because the current value is going to change on every "interval" as long as it should be running, then your code will start and stop a new timer on every render. You can see this in action here:
https://codesandbox.io/s/03xkkyj19w
You can change setInterval to be setTimeout and you will get the exact same behaviour. setTimeout is not a persistent clock, but it doesn't matter since they both get cleaned up anyways.
If you do not want to start any timer at all, then put the condition before setInterval not inside of it.
useEffect(
() => {
let id;
if (run) {
id = setInterval(() => {
setValue(value + 1)
}, 1000);
}
return () => {
if (id) {
alert(id) // notice this runs on every render and is different every time
clearInterval(id);
}
};
}
);
So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on auto and current to work
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
Conditionally executing update to state — it does not require useEffect dependencies
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, autoInterval);
return _ => clearInterval(interval);
});
Both solutions work if we replace setInterval by setTimeout
You could use useTimeout hook that returns true after specified number of milliseconds.
https://github.com/streamich/react-use/blob/master/docs/useTimeout.md

Resources