react hooks and setInterval - reactjs

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

Related

React setInterval is re-initialized to often

I've a hard time understanding how setInterval works. My main problem is that an interval is re-initialized too often.
Basically, I want a context-sensitive sidebar to be modfied by MainElement, and I want this sidebar to do something at a regular base. In the real scenario there the timer gets cancelled when unmounting ofc.
import { useEffect, useState } from 'react';
// This is the component called from outside
export const MainLayout = () => {
const [element2Content, setElement2Content] = useState<string | null>(null);
return (
<>
<MainElement setElement2Content={setElement2Content}>
Element1
</MainElement>
{element2Content && <Sidebar content={element2Content} />}
</>
);
};
// This component manipulates the sidebar via useEffect
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
({ setElement2Content, children }) => {
useEffect(() => setElement2Content('content set from element 1'), []);
return <div>{children}</div>;
};
// This component utilizes the setInterval, but doesn't work as expected
const Sidebar: React.FC<{ content: string }> = ({ content }) => {
const [calls, setCalls] = useState(0);
useEffect(() => {
setInterval(() => {
console.log('interval called for', calls + 1, 'times');
setCalls(calls + 1);
}, 1000);
}, []);
return <div>{`content${content}, calls: ${calls}`}</div>;
};
The log is just interval called for 1 times in a loop.
In the browser I see the components rendered, and I see interval called for 0 times being changed to interval called for 1 times, where it stops.
So I'm wondering: Why does it stop at 1? It seems like setInterval gets reset all the time.
To understand the behavior of a timer a bit more, I changed my MainElement to
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
({ setElement2Content, children }) => {
useEffect(() => setElement2Content('content set in element 1'), []);
useEffect(() => {
setInterval(() => {
console.log('interval called from mainelement');
}, 1000);
}, []);
return <div>{children}</div>;
};
Now, for some reason the MainElement is also re-rendered repeatedly, and so is the sidebar. The console logs
interval called in mainelement
interval called for 1 times
I'ld be grateful for any ideas or explanations!
The interval is running correctly, but in your interval function calls is never being updated passed 1. This is because the calls variable in the setInterval function is stale (i.e. it is always equal to 0 when the function is invoked, so setCalls(call + 1) will always update calls to 1.) This staleness is because the calls variable in the function is captured in the closure formed when the effect is run during the first render of the component and never updated thereafter.
A quick way to address this is to use the functional version of the setState call:
useEffect(() => {
setInterval(() => {
console.log('interval called for', calls + 1, 'times');
setCalls(prevCalls => prevCalls + 1);
}, 1000);
}, []);

How to update state using setInterval on functional components in React

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

Counter in react-js run only once when Component Re-Render?

I tried setting a timer to a function I want to be called every 2 seconds:
// start timer
if(!timerStarted){
tid = setInterval(ReloadMessage, 2000);
timerStarted = true
}
But I want this timer instance to only be ran once (hence the !timerStarted)
Unfortunately, this gets ignored when the component rerenders from the state change.
I tried ending the timer but I found no way to know in advance when the state changes.
So I tried:
//my Functional component useEffect
React.useEffect(()=>{
(async () => {
// start timer
if(!timerStarted){
tid = setInterval(ReloadMessage, 2000);
timerStarted = true
}
})()
},[])
Thinking this would make the effect be called only once upon component load, but this ended up not calling the timer at all (Maybe because I also have a second effect with dependencies here?)
How do I make sure this timer is set off once and only once, no matter what the user does?
Using an empty dependencies array for your effect, will ensure that it only runs once. With that in mind, it's kind of irrelevant to track that a timerStarted.
The usage of this flag (provided it's a variable scoped to the component) even indicates that it actually should be a dependency, which your linter, if you have one, should notify you of. Though as stated above you don't need it, and it would only make things more complicated.
Also the async IIEF is not needed as you don't await anything.
So, all in all, this should be enough:
React.useEffect(()=>{
const tid = setInterval(ReloadMessage, 2000);
return () => {
clearInterval(tid);
};
},[]);
As per the comments, here's a simple demo of how you can use a ref, to get access to some dependency that you absolutely do not want to list as a dependency. Use this sparingly and only with good consideration, because it often hints at a problem that started somewhere else (often a design problem):
import { useEffect, useRef, useState } from 'react';
const Tmp = () => {
const [counter, setCounter] = useState(0);
const counterRef = useRef(counter);
useEffect(() => {
counterRef.current = counter;
}, [counter]);
useEffect(() => {
const t = setInterval(() => {
console.log('Invalid', counter); // always *lags behind* because of *closures* and
// will trigger a linter error, as it should actually be a dependency
console.log('Valid', counterRef.current); // current counter
}, 2000);
return () => {
clearInterval(t);
};
}, []);
return (
<div>
<div>
<button onClick={() => setCounter(current => current - 1)}>-</button>
{counter}
<button onClick={() => setCounter(current => current + 1)}>+</button>
</div>
</div>
);
};
export default Tmp;

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.

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);

Resources