Effects: useEffect
A component should only compute markup from props and state. Everything beyond that — changing the tab title, saving data to localStorage, calling a server, starting a timer — is called a side effect. Such code goes into the useEffect hook: React runs it after updating the page.
A first effect
Let us show the number of tasks left in the tab title. In src/App.jsx:
// src/App.jsx
import { useEffect, useState } from 'react';
export default function App() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Call Oleg', done: false },
]);
const left = todos.filter((todo) => !todo.done).length;
useEffect(() => {
document.title = `Tasks: ${left}`;
});
return <p>Tasks left: {left}</p>;
}The browser tab shows "Tasks: 2". An effect without a second argument runs after every render.
The dependency array
The second argument of useEffect is the dependency array. It tells React when to run the effect again:
// src/App.jsx
useEffect(() => {
document.title = `Tasks: ${left}`;
}, [left]);Now the effect runs after the first render and then only when left changes. Adding a task updates the title; typing in a field does not.
| Form | When it runs |
|---|---|
useEffect(fn) | After every render |
useEffect(fn, []) | Once, after the first render |
useEffect(fn, [a, b]) | After the first render and when a or b changes |
Put into the array every prop and state variable used inside the effect. The linter in a Vite project warns you when something is missing.
Saving to localStorage
Another effect saves the list on every change:
// src/App.jsx
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);To load the list back when the page opens, read the initial value in useState through a function — it runs only on the first render:
// src/App.jsx
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});Add a task and reload the page — it stays in the list.
When you do not need an effect
If a value can be computed from props and state, no effect is needed — compute it during rendering. Reactions to a click belong in the event handler. An effect is only for syncing with the outside world: DOM outside React, storage, the network, timers.
Common mistake
In development, StrictMode from src/main.jsx deliberately runs every effect twice to check that it cleans up correctly. If a message from an effect appears twice in the console, it is not a bug — in the built site the effect runs once. Do not remove StrictMode to get rid of it: teach the effect to clean up after itself instead, which is the next lesson.