useEffect & Side Effects
Rendering a component should be a pure calculation: given the same props and state, it returns the same JSX, with no side effects like network requests or manually touching the DOM. useEffect is where that "impure" work belongs instead — code that needs to run after React has rendered.
A basic effect
JSX PageTitle.jsx
import { useState, useEffect } from "react";
function PageTitle() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <button onClick={() => setCount(count + 1)}>+1</button>;
}
Rendered output
A "+1" button. The browser tab's title updates to "Count: 1", "Count: 2", etc. as it's clicked.
The function passed to useEffect runs after the render commits to the screen. The array [count] is the dependency array — it tells React "only re-run this effect when count changes," instead of after every single render.
The dependency array's three forms
JSX effect forms
useEffect(() => { /* ... */ }); // runs after every render
useEffect(() => { /* ... */ }, []); // runs once, after the first render
useEffect(() => { /* ... */ }, [count]); // runs after the first render, and again whenever count changes
Cleaning up an effect
If an effect sets something up — a timer, a subscription, an event listener — returning a function from it tells React how to tear that down, which runs before the effect runs again and when the component is removed from the page:
JSX Clock.jsx
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id);
}, []);
return <p>{time.toLocaleTimeString()}</p>;
}
Rendered output
The current time, updating once per second while the component is on the page. The interval is cleared automatically if Clock is removed.
Note: a "stale closure" is the classic
useEffect bug — if your effect uses a value but that value is missing from the dependency array, the effect keeps using whatever that value was when the effect was first created, not its current value. React's linter (via eslint-plugin-react-hooks) flags this for you; when it warns about a missing dependency, it's almost always right.