Skip to content
WANCODE
BM

Effects and Common Pitfalls

25 minIntermediateReact

In this lesson

  • Use useEffect to 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.

JSX
import { useEffect } from "react";

useEffect(() => {
  // runs AFTER React has updated the screen
  document.title = "Focus: " + task;
}, [task]); // ← dependency array: re-run when task changes

The 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 whenever a or b changed. 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.

JSX
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

JSX
useEffect(() => {
  setCount(count + 1); // no array → runs every render
});

Runs only when it should

JSX
useEffect(() => {
  document.title = count + " clicks";
}, [count]); // only when count changes

You 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

JSX
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(first + " " + last);
}, [first, last]);

Just calculate it

JSX
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

Q1

When does useEffect(fn, []) run?

Q2

Your timer effect starts setInterval but returns nothing. What happens when you press Pause?

Q3

You need total = sum of the prices in cart (which is state). What's best?

What you learned

  • useEffect runs 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.
Preview

Console output from your code appears here.

Step 1 of 3