Rendering should be a pure calculation: props + state → JSX. Anything that does something (network, setInterval, talking to document) belongs in useEffect.
import { useState, useEffect } from 'react';
function Title({ count }) {
useEffect(() => {
document.title = 'Clicks: ' + count;
}, [count]);
return <p>{count}</p>;
}- Empty dependency array
[]— run after the first paint only (mount). [count]— run after mount and whenevercountchanges.- Omit the array — run after every render (rarely what you want).
Cleanup
Return a function to undo the effect (clear timers, abort fetch, unsubscribe):
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);Fetching
useEffect(() => {
const ctrl = new AbortController();
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal: ctrl.signal })
.then((r) => r.json())
.then(setTodo)
.catch((err) => {
if (err.name !== 'AbortError') setError(err.message);
});
return () => ctrl.abort();
}, []);In React 18+ Strict Mode (on in Vite’s default main.jsx), React mounts, unmounts, and remounts once in development to catch missing cleanups. You may see two network calls in dev. That is expected; production mounts once.