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
andsetTimeout
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 thecurrent
dependency causes that effect to dispose and re-run every time thecurrent
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 becausenext()
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 thecurrent
andauto
vars as dependencies. So to re-state part of your answer, do this: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:
However, you may be wondering how to use a more exact interval, given the fact that
setTimeout
can drift more thansetInterval
. Here is one method, again, generic without using the OP's code:What is happening here? Well, to get
setInterval
's callback to always refer to the currently acceptable version ofsetCounter
we need some mutable state. React gives us this withuseRef
. TheuseRef
function will return an object that has acurrent
property. We can then set that property (which will happen every time the component re-renders) to the current versions ofcounter
andsetCounter
.Then, to keep the interval from being disposed of on each render, we add a dependency to
useEffect
that is guaranteed never to change. In my case, I like using the string"once"
to indicate that I am forcing this effect to be set up only once. The interval will still be disposed of when the component is unmounted.So applying what we know to the OP's original question, you could use
setInterval
for a less-likely-to-drift slideshow like this:You could use
useTimeout
hook that returnstrue
after specified number of milliseconds.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 besetTimeout
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.So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on
auto
andcurrent
to workConditionally executing update to state — it does not require useEffect dependencies
Both solutions work if we replace
setInterval
bysetTimeout