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

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.

Related

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

Is it okay to change boolean which is a hook and dependency of useEffect in React?

I am working on CSS Transition on React that it automatically unmount after 2 seconds. I am thinking about using useEffect and useState to solve this problem.
I know that changing dependencies inside useEffect causes infinite loop.
For example, the code below will cause infinite loop.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1);
},[count]);
But I think infinite loop won't happen if I set dependency to boolean and set if statement inside useEffect just like the code below.
const [showStatus, setshowStatus] = useState(false);
useEffect(() => {
const timeId = setTimeout(() => {
if (showStatus === true){
setshowStatus(false)
}
}, 2000)
return (() => {clearTimeout(timeId)})
}, [showStatus]);
I am relatively new to React so I am worried about this code. Do I have any problems using this code?
I think it's answered here better
React hooks - right way to clear timeouts and intervals
import { useState, useEffect } from "react";
const delay = 5;
export default function App() {
const [show, setShow] = useState(false);
useEffect(
() => {
let timer1 = setTimeout(() => setShow(true), delay * 1000);
// this will clear Timeout
// when component unmount like in willComponentUnmount
// and show will not change to true
return () => {
clearTimeout(timer1);
};
},
// useEffect will run only one time with empty []
// if you pass a value to array,
// like this - [data]
// than clearTimeout will run every time
// this value changes (useEffect re-run)
[]
);
return show ? (
<div>show is true, {delay}seconds passed</div>
) : (
<div>show is false, wait {delay}seconds</div>
);
}

How to deal with stale state values inside of a useEffect closure?

The following example is of a Timer component that has a button (to start the timer), and two tags that display the number of elapsed seconds, and the number of elapsed seconds times 2.
However, it does not work (CodeSandbox Demo)
The Code
import React, { useState, useEffect } from "react";
const Timer = () => {
const [doubleSeconds, setDoubleSeconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
setDoubleSeconds(seconds * 2);
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The Problem
Inside the useEffect call, the "seconds" value will always be equal to the its value when the useEffect block was last rendered (when isActive last changed). This will result in the setDoubleSeconds(seconds * 2) statement to fail. The React Hooks ESLint plugin gives me a warning regarding this problem that reads:
React Hook useEffect has a missing dependency: 'seconds'. Either include it or remove the dependency array. You can also replace
multiple useState variables with useReducer if 'setDoubleSeconds'
needs the current value of 'seconds'.
(react-hooks/exhaustive-deps)eslint
And correctly so, adding "seconds" to the dependency array (and changing setDoubleSeconds(seconds * 2) to setDoubleSeconds((seconds + 1) * ) will render the correct results. However, this has a nasty side effect of causing the interval to be created and destroyed on every render (the console.log("Destroying Interval") fires on every render).
So now I am looking at the other recommendation from the ESLint warning "You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'".
I do not understand this recommendation. If I create a reducer and use it like so:
import React, { useState, useEffect, useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "SET": {
return action.seconds;
}
default: {
return state;
}
}
};
const Timer = () => {
const [doubleSeconds, dispatch] = useReducer(reducer, 0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
dispatch({ type: "SET", seconds });
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The problem of stale values will still exist (CodeSandbox Demo (using Reducers)).
The Question(s)
So what is the recommendation for this scenario? Do I take the performance hit and simply add "seconds" to the dependency array? Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there? Do I merge "seconds" and "doubleSeconds" into a single state object? Do I use refs?
Also, you might be thinking "Why don't you simply change <h3>Seconds x2: {doubleSeconds}</h3>" to <h3>Seconds x2: {seconds * 2}</h3> and remove the 'doubleSeconds' state?". In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable.
Thanks!
You can access a value inside an effect callback without adding it as a dep in a few ways.
setState. You can tap the up-to-date value of a state variable through its setter.
setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
Ref. You can pass a ref as a dependency and it'll never change. You need to manually keep it up to date, though.
const secondsRef = useRef(0);
const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);
You can then use secondsRef.current to access seconds in a block of code without having it trigger deps changes.
setDoubleSeconds(secondsRef.current * 2);
In my opinion you should never omit a dependency from the deps array. Use a hack like the above to make sure your values are up-to-date if you need the deps not to change.
Always first consider if there's some more elegant way to write your code than hacking a value into a callback. In your example doubleSeconds can be expressed as a derivative of seconds.
const [seconds, setSeconds] = useState(0);
const doubleSeconds = seconds * 2;
Sometimes applications aren't that simple so you may need to use the hacks described above.
Do I take the performance hit and simply add "seconds" to the dependency array?
Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there?
Do I merge "seconds" and "doubleSeconds" into a single state object?
Do I use refs?
All of them work correctly, although personally I would rather choose the second approach:
useEffect(() => {
setDoubleSeconds(seconds * 2);
}, [seconds]);
However:
In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable
That is questionable. Child component might be implemented like the following:
const Child = ({second}) => (
<p>Seconds: {second}s</p>
);
And parent component should look like the following:
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// change seconds
}, []);
return (
<React.Fragment>
<Child seconds={second} />
<Child seconds={second * 2} />
</React.Fragment>
);
This would be a more clear and concise way.

Why does my setInterval run only once in react hooks?

Why does this code only triggers the setInterval once and then stops...
const MainBar = ()=> {
const [clock, setClock] = useState("")
useEffect(() => {
const interval = setInterval(setClock(clockUpdate()), 1000);
console.log('Im in useEffect', clock)
});
...
Whereas passing it into another function makes it work each second like so ?
const MainBar = ()=> {
const [clock, setClock] = useState("")
useEffect(() => {
const interval = setInterval(()=>{setClock(clockUpdate())}, 1000);
console.log('Im in useEffect', clock)
});
...
Sorry I'm new to hooks and javascript.
setInterval requires a function to be passed for it to execute. It will execute the given function every second in this case. () => { setClock(clockUpdate()) } is actually an anonymous function; a function without a name. If you'd give it a proper name, it'd look like function updater() { setClock(clockUpdate()); }.
setInterval(setClock(clockUpdate()), 1000) doesn't work because setClock(clockUpdate()) is already executed, even before it is passed to setInterval. It cannot schedule it to run again, because it's not a function, it is a result already.
You can try this by adding the second parameter in useEffect which means if the clock changes, useEffect will run again
useEffect(() => {
const interval = setInterval(()=>{setClock(clockUpdate())}, 1000);
console.log('Im in useEffect', clock)
}, [clock]);

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