The useEffect
Hook in React enables the execution of side effects in your components. Common side effects include: data fetching, direct DOM manipulation, and setting up timers.
Using useEffect
useEffect
takes two arguments, with the second one being optional:
useEffect(<effect function>, <dependencies>);
We’ll use a timer as an illustrative example.
Example
Using setTimeout()
to count 1 second after the initial render:
import { useState, useEffect } from "react"; import ReactDOM from "react-dom/client"; function Countdown() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { setCount(prevCount => prevCount + 1); }, 1000); }); return <h1>The component has rendered {count} times!</h1>; } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<Countdown />);
However, this keeps counting continuously.
useEffect
executes on every render. This means that when count
changes, the component re-renders, triggering the effect again. This isn’t desirable. There are ways to control when side effects should run.
You should always provide the second parameter, which can take an array of dependencies.
Controlling the Execution
Example 1: No dependencies:
useEffect(() => { //Executes on every render });
Example 2: Empty array:
useEffect(() => { //Executes only on the initial render }, []);
Example 3: With dependencies (props or state values):
useEffect(() => { //Executes on initial render and whenever any dependency changes }, [prop, state]);
To fix our timer issue, let’s ensure the effect only runs on the initial render.
Example
Run the effect only once:
import { useState, useEffect } from "react"; import ReactDOM from "react-dom/client"; function Countdown() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { setCount(prevCount => prevCount + 1); }, 1000); }, []); // <- empty array dependency return <h1>The component has rendered {count} times!</h1>; } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<Countdown />);
Example
Here is an example where the useEffect
Hook depends on a variable. The effect re-runs whenever the count
variable updates:
import { useState, useEffect } from "react"; import ReactDOM from "react-dom/client"; function Multiplier() { const [count, setCount] = useState(0); const [result, setResult] = useState(0); useEffect(() => { setResult(count * 2); }, [count]); // <- `count` is the dependency return ( <> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increase</button> <p>Result: {result}</p> </> ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<Multiplier />);
When there are multiple dependencies, they should all be included in the useEffect
dependency array.
Cleaning Up Effects
Certain effects need to be cleaned up to prevent memory leaks. This includes timeouts, subscriptions, event listeners, and other effects that are no longer necessary.
Cleanup is done by returning a function at the end of the useEffect
Hook.
Example
Clean up the timer:
import { useState, useEffect } from "react"; import ReactDOM from "react-dom/client"; function Countdown() { const [count, setCount] = useState(0); useEffect(() => { let timer = setTimeout(() => { setCount(prevCount => prevCount + 1); }, 1000); return () => clearTimeout(timer); }, []); return <h1>The component has rendered {count} times!</h1>; } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<Countdown />);