Why? Ensures compatibility with strict / concurrent rendering modes.
React calls your component’s render function a lot—sometimes twice in development (Strict Mode) and, with Concurrent Rendering, potentially even more. If that render kicks off a network request, mutates global data, or directly manipulates the DOM, you’ll get duplicate calls, race conditions, or even infinite loops. Beginners often sprinkle “just one fetch” or localStorage.setItem right inside render, unaware that React expects render to be a pure calculation: same inputs → same output → zero side‑effects.
import { useEffect, useState } from "react";
export default function RandomJoke() {
const [joke, setJoke] = useState<string | null>(null);
// Side‑effect (network fetch) lives in useEffect, not render.
useEffect(() => {
fetch("https://api.chucknorris.io/jokes/random")
.then((res) => res.json())
.then((data) => setJoke(data.value));
}, []); // run once after initial mount
// Render stays pure—just reflects current state.
return (
<section className="rounded-xl border p-4">
{joke ?? "Loading a hilarious joke…"}
</section>
);
}
What’s happening?
Render merely transforms joke state into UI. All side‑effects (fetch) are quarantined in useEffect, ensuring no duplicate requests on re‑render.
function RandomJokeImpure() {
const [joke, setJoke] = useState<string | null>(null);
// ❌ Fetching during render—runs every render in Dev Strict Mode.
if (!joke) {
fetch("https://api.chucknorris.io/jokes/random")
.then((res) => res.json())
.then((data) => setJoke(data.value));
}
return (
<section className="rounded-xl border p-4">
{joke ?? "Loading a hilarious joke…"}
</section>
);
}
// In development, Strict Mode double‑invokes render → two fetches. In
// concurrent mode, React may start and abandon renders, leaking requests.
useEffect (or useLayoutEffect for layout‑critical work) to perform asynchronous calls, subscriptions, or DOM writes.setState during render triggers an immediate re‑render loop.