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.