The useEffect
hook is called in a component after the first render and every time the component updates. By the timer useEffect
is called, the real DOM would have been updated to reflect any state changes in the component.
Let’s take a look at a quick example to practically observe this behavior:
import { useEffect, useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Button Clicks: ${count}`;
}, [count])
const handleAdd = () => {
setCount((count) => count + 1);
}
return (
<div>
Count: {count}
<br />
<button onClick={handleAdd}>Add</button>
</div>
);
}
The first time the component renders from page loading, useEffect
is called and uses the document.title
property to set the page title to a string whose value depends on a count
state variable.
If you watch closely, you’ll see that the page title was initially “Coding Beauty React Tutorial”, before the component was added to the DOM and the page title was changed from the useEffect
call.
useEffect
will also be called when the state is changed:
As you might know, useEffect
accepts an optional dependency array as its second argument. This array contains the state variables, props, and functions that it should watch for changes.
In our example, the dependency array contains only the count
state variable, so useEffect
will only be called when the count
state changes.
useEffect
called twice in React 18?
React 18 introduced a new development-only check to Strict Mode. This new check automatically unmounts and remounts a component when it mounts for the first time, and restores the previous state on the second mount.
Note: The check was added because of a new feature that will be added to React in the future. Learn more about it here.
This means that the first render causes useEffect
to actually be called two times, instead of just once.
Here’s an example that lets us observe this new behavior.
import { useEffect, useState } from 'react';
export default function App() {
const [time, setTime] = useState(0);
useEffect(() => {
setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
}, []);
return (
<div>
Seconds: {time}
</div>
);
}
It’s a basic time-counting app that implements the core logic that can be used to build timer and stopwatch apps.
We create an interval listener with setInterval()
that increments a time
state by 1 every second. The listener is in a useEffect
that has an empty dependency array ([]
), because we want it to be registered only when the component mounts.
But watch what happens when we check out the result on the web page:
The seconds are going up by 2 every second instead of 1! Because React 18’s new check causes the component to be mounted twice, an useEffect
is called accordingly.
We can fix this issue by unregistering the interval listener in the useEffect
‘s cleanup function.
useEffect(() => {
const timer = setInterval(() => {
setTime((prevTime) => prevTime);
});
// 👇 Unregister interval listener
return () => {
clearInterval(timer);
}
}, [])
The cleanup function that the useEffect
callback returns is called when the component is mounted. So when React 18 does the compulsory first unmounting, the first interval listener is unregistered with clearInterval()
. When the second interval listener is registered on the second mount, it will be the only active listener, ensuring that the time
state is incremented by the correct value of 1
every second.
Note that even if we didn’t have this issue of useEffect
being called twice, we would still have to unregister the listener in the cleanup function, to prevent memory leaks after the component is removed from the DOM.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.