Effects and Common Pitfalls
25 minIntermediateReact
In this lesson
- ▸Use
useEffectto synchronise with something outside React (title, timer, storage) - ▸Choose the right dependency array and write a cleanup function
- ▸Avoid infinite loops and unnecessary effects by deriving values during render
The big idea
Rendering should only calculate UI. When you need to reach outside React — the tab title, a timer, the browser's storage — you put that code in useEffect, which runs after React updates the screen.
So far everything has lived inside React: state goes in, JSX comes out. But apps also need to talk to things React doesn't manage: document.title, setInterval, localStorage, a chat connection. Those are side effects, and useEffect is where they belong.
import { useEffect } from "react";
useEffect(() => {
// runs AFTER React has updated the screen
document.title = "Focus: " + task;
}, [task]); // ← dependency array: re-run when task changesThe dependency array
useEffect(fn)— no array: runs after every render. Rarely what you want.useEffect(fn, [])— empty array: runs once, after the first render (when the component mounts).useEffect(fn, [a, b])— runs after the first render and again wheneveraorbchanged. List every value from the component that the effect uses.
Cleanup
If your effect starts something — an interval, a subscription, an event listener — return a function that stops it. React calls that cleanup before running the effect again, and when the component disappears.
useEffect(() => {
if (!running) return; // nothing to start
const id = setInterval(() => {
setSeconds((s) => s + 1); // updater form: always the latest value
}, 1000);
return () => clearInterval(id); // cleanup: stop this interval
}, [running]);Why s => s + 1?
The interval callback was created during one render, so seconds inside it is stuck at that render's value. setSeconds(seconds + 1) would keep setting 1. The updater form asks React for the current value each time.
In a real React project with <StrictMode>, React runs each effect, its cleanup, and the effect again once in development. It's on purpose: missing cleanups show up straight away. (The WANCode preview doesn't use StrictMode.)
Pitfall: the infinite loop
Setting state inside an effect causes a re-render. If the effect runs after every render, it sets state again, which renders again… forever. React detects this and logs Maximum update depth exceeded.
Loops forever
useEffect(() => {
setCount(count + 1); // no array → runs every render
});Runs only when it should
useEffect(() => {
document.title = count + " clicks";
}, [count]); // only when count changesYou might not need an effect
If a value can be calculated from props or state, just calculate it during render. Copying it into state with an effect is slower (an extra render) and easy to get out of sync.
Unnecessary effect
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(first + " " + last);
}, [first, last]);Just calculate it
const fullName = first + " " + last;The preview has its own emulated localStorage for each lesson. It's kept when the preview reloads, so you can see saved values survive — just like a real browser.
Import useEffect and add an effect that sets document.title to Focus: <task> whenever the task changes — or Focus Timer when the task is empty. Use [task] as the dependency array.
Checks
- The tab title follows the task
- An empty task shows
Focus Timer - The title is set in an effect
Check understanding
What you learned
- ✓
useEffectruns after render to sync with things outside React: titles, timers, storage, connections. - ✓The dependency array decides when it re-runs; list every value the effect uses.
- ✓Return a cleanup to stop whatever you started (
clearInterval, remove listeners). - ✓Setting state in an effect with no array loops forever; values you can calculate don't need an effect at all.
Console output from your code appears here.