Intermediate
Effects & Data
Effects keep a component in sync with the world outside React. Refs let you hold on to values and DOM elements without re-rendering.
Rendering must be pure
React may call your component function many times, and sometimes calls it twice in development on purpose to catch mistakes. The body of a component should therefore only calculate the output. It should not start timers, change the DOM directly or send network requests.
Work of that kind is a side effect. It belongs either in an event handler, when it is caused by something the user did, or in an effect, when it is caused by the component appearing on screen.
The dependency array
The second argument to useEffect tells React when to run the effect again:
| You write | The effect runs |
|---|---|
useEffect(fn) | After every render. |
useEffect(fn, []) | Once, after the component first appears (it mounts). |
useEffect(fn, [a, b]) | After mounting, and again whenever a or b has changed. |
List every prop, state variable or other reactive value the effect reads. The eslint-plugin-react-hooks linter warns you when one is missing. Leaving values out is a common source of bugs, because the effect then works with stale data.
Cleaning up
An effect can return a cleanup function. React calls it before running the effect again and when the component is removed:
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h2>Welcome to {roomId}</h2>;
}
When roomId changes, React disconnects from the old room and then connects to the new one. In development, React mounts each component twice, running the effect, its cleanup and the effect again. If you see something happen twice in development, check that the effect cleans up after itself.
You might not need an effect
Effects are for synchronizing with external systems. Beginners often use them for jobs that don't need one:
Values derived from props or state
// ✘ Extra state, an extra render, and it can fall out of sync
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(first + " " + last);
}, [first, last]);
// ✔ Just calculate it while rendering
const fullName = first + " " + last;
If the calculation is slow, wrap it in useMemo instead of copying the result into state.
Responding to user actions
// ✘ Runs whenever `submitted` changes, however it got there
useEffect(() => {
if (submitted) sendAnalytics("form_submit");
}, [submitted]);
// ✔ Do it in the handler, where you know what happened
function handleSubmit() {
sendAnalytics("form_submit");
setSubmitted(true);
}
Resetting state when a prop changes
Rather than using an effect to clear state when the user switches to another profile, give the component a key. React treats a new key as a different component and starts it with fresh state:
<ProfileForm key={userId} userId={userId} />
Fetching data
When you fetch inside an effect, handle loading and errors, and ignore responses that arrive out of order:
function UserCard({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
setUser(null);
setError(null);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => { if (!ignore) setUser(data); })
.catch(err => { if (!ignore) setError(err); });
return () => { ignore = true; };
}, [userId]);
if (error) return <p>Couldn't load user.</p>;
if (!user) return <p>Loading…</p>;
return <h3>{user.name}</h3>;
}
The ignore flag guards against a race condition. Suppose the user switches from user 1 to user 2 quickly and the response for user 1 arrives last. Without the flag, it would overwrite the data for user 2.
In real apps, consider a data-fetching library such as TanStack Query, or your framework's built-in data loading. They handle caching, retries, deduplication and race conditions for you.
Refs
useRef gives you a box whose .current value survives between renders. Unlike state, changing it does not cause a re-render.
Accessing a DOM element
import { useRef } from "react";
function SearchBar() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} placeholder="Search…" />
<button onClick={() => inputRef.current.focus()}>
Focus search
</button>
</>
);
}
Remembering a value without re-rendering
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
return (
<>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
Note setSeconds(s => s + 1). Passing an updater function means the interval always works from the latest value instead of the seconds it saw when it started.
Don't read or write ref.current during rendering. Use it in event handlers and effects only. If a value should appear on screen, it belongs in state.