useEffect & the Dependency Array

Reviewed & published by Brayan K

useEffect lets a component run side effects — work that reaches outside React, like setting the document title, starting a timer, or subscribing to something — after it renders. The second argument, the dependency array, controls exactly when the effect re-runs, and the function it returns handles cleanup.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ The Dependency Array Controls When It Runs

After each render, React compares every value in the dependency array to its value from the previous render. If any changed, the effect runs again. Three shapes you'll use constantly:

useEffect(() => { /* ... */ });          // no array  -> runs after EVERY render
useEffect(() => { /* ... */ }, []);      // empty     -> runs ONCE (after first render)
useEffect(() => { /* ... */ }, [count]); // [count]   -> runs when count changes
// React compares each dependency to its value LAST render.
// If any changed, the effect runs. Let's simulate that comparison.

function shouldRun(prevDeps, nextDeps) {
  // runs if ANY dependency changed
  return nextDeps.some((dep, i) => dep !== prevDeps[i]);
}

console.log(shouldRun([0], [0]));   // false  -> deps same, skip effect
console.log(shouldRun([0], [1]));   // true   -> count changed, run effect
console.log(shouldRun([1, "a"], [1, "b"]));   // true  -> second dep changed

// Empty deps [] never change, so [] vs [] is always "false" after mount:
console.log(shouldRun([], []));     // false  -> run once, then never again

Your turn — write the comparison React uses for a single dependency. It should be true only when the value changed:

// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// Decide whether the effect should re-run. It re-runs when the
// "count" dependency CHANGED between renders.

function shouldRun(prevCount, nextCount) {
  // 1) Return true only when the value changed.
  return ___;        // 👉 compare prevCount and nextCount
}

console.log(shouldRun(0, 0));   // expect false
console.log(shouldRun(0, 1));   // expect true
console.log(shouldRun(5, 5));   // expect false

// ✅ Expected output:
//    false
//    true
//    false

2️⃣ Effects Run After the Browser Paints

React renders your component (computing JSX), lets the browser paint, and then runs your effects. That ordering is why effects never block the visible update — and why you read fresh DOM measurements or set the title after render, not during it.

// ORDER OF EVENTS: React renders, paints the screen, THEN runs effects.
const log = [];

function render() { log.push("1. render (compute JSX)"); }
function paint()  { log.push("2. browser paints screen"); }
function effect() { log.push("3. effect runs (side effect)"); }

render();
paint();
effect();

log.forEach(line => console.log(line));
// 1. render (compute JSX)
// 2. browser paints screen
// 3. effect runs (side effect)
// Effects run AFTER paint, so they never block the visible update.

3️⃣ Cleanup — Undo Before You Redo

If your effect starts something that keeps running — a timer, an event listener, a subscription — you must stop it. Return a function from the effect; React runs it before the next effect and when the component unmounts.

useEffect(() => {
  const id = setInterval(tick, 1000);   // setup: start the timer
  return () => clearInterval(id);       // cleanup: stop it
}, []);                                 // start once, stop on unmount
// CLEANUP: the function you return runs before the next effect
// and on unmount. Watch the order across two "renders".

const log = [];
function effect(id) {
  log.push("setup " + id);            // e.g. start a timer
  return () => log.push("cleanup " + id);  // stop that timer
}

const cleanup1 = effect(1);   // first render: setup 1
cleanup1();                   // before re-running: cleanup 1
const cleanup2 = effect(2);   // re-render: setup 2
cleanup2();                   // unmount: cleanup 2

log.forEach(line => console.log(line));
// setup 1
// cleanup 1
// setup 2
// cleanup 2

🧩 Reorder Challenge

These lines form a complete effect that subscribes to a timer and cleans it up. Put them in the right order:

A.   const id = setInterval(tick, 1000);
B.   useEffect(() => {
C.   }, []);
D.   return () => clearInterval(id);

Common Errors (and the fix)

Pro Tips

📋 Quick Reference

Dependency arrayWhen the effect runs
(omitted)After every render
[]Once, after the first render
[count]When count changes
return () => {...}Cleanup: before re-run & on unmount

🧠 Quick Recall

1. An effect has deps []. The component re-renders 5 times. How many times does the effect run?

Once. An empty array never changes, so the effect runs only after the first render.

2. You write useEffect(() => setN(n + 1)) with no deps array. What happens?

Infinite loop. It runs after every render, sets state, which re-renders, which runs it again.

3. In what order do render, paint, and the effect happen?

Render → paint → effect. Effects run after the screen is painted.

Mini-Challenge: Title-Only-On-Change

Model an effect with [count]: it should "set the title" only when count actually changes. Run it and check your output.

// 🎯 MINI-CHALLENGE: model the document.title effect
//
// An effect with [count] updates the title only when count changes.
// 1. Start prevCount = null (first render always runs).
// 2. For each value in clicks, decide if the effect runs (value changed),
//    and if so push "Set title: " + value to results.
// 3. console.log every line in results.
//
// ✅ Expected output:
//    Set title: 0
//    Set title: 1
//    Set title: 3

const clicks = [0, 0, 1, 1, 3];
let prevCount = null;
const results = [];

// your code here

🎉 Lesson Complete

Practice quiz

What does the second argument to useEffect (the dependency array) control?

  • How fast the effect runs
  • Whether the component is memoized
  • When the effect re-runs after a render
  • The order of multiple effects in a component

Answer: When the effect re-runs after a render. After each render React compares every value in the dependency array to its previous value; if any changed, the effect runs again.

An effect has an empty dependency array []. The component re-renders 5 times. How many times does the effect run?

  • Once, after the first render
  • 5 times
  • 0 times
  • 6 times

Answer: Once, after the first render. An empty array never changes, so the effect runs only once after the first render and never again.

What happens if you omit the dependency array entirely?

  • The effect never runs
  • The effect runs only once
  • React throws an error
  • The effect runs after every render

Answer: The effect runs after every render. With no dependency array, React runs the effect after EVERY render, which often causes infinite loops if the effect also sets state.

What does returning a function from useEffect do?

  • It returns a value to the parent component
  • It defines the cleanup that runs before the next effect and on unmount
  • It memoizes the effect
  • It schedules the effect to run earlier

Answer: It defines the cleanup that runs before the next effect and on unmount. The returned function is the cleanup: React runs it before re-running the effect and when the component unmounts, so you can clear timers, listeners, and subscriptions.

In what order do render, browser paint, and the effect happen?

  • Render, then paint, then effect
  • Effect, then render, then paint
  • Render, then effect, then paint
  • Paint, then render, then effect

Answer: Render, then paint, then effect. React renders (computes JSX), the browser paints the screen, and THEN effects run — so effects never block the visible update.

You write useEffect(() => setN(n + 1)) with no dependency array. What happens?

  • It runs once and stops
  • Nothing, because setN is ignored
  • An infinite loop of renders
  • It runs only when n changes

Answer: An infinite loop of renders. With no deps it runs after every render; setting state triggers a re-render, which runs the effect again — an infinite loop.

An effect reads a prop but that prop is not in the dependency array. What is the likely bug?

  • The effect runs too often
  • The effect sees a stale value from an old closure
  • React refuses to render
  • The cleanup never runs

Answer: The effect sees a stale value from an old closure. If a reactive value the effect reads is missing from deps, the effect keeps the old closure and uses stale data. Add every reactive value it reads.

Why might an effect run twice in development?

  • A React bug
  • Because the deps array is empty
  • Because the component has two effects
  • React 18 Strict Mode mounts, unmounts, and remounts to surface missing cleanup

Answer: React 18 Strict Mode mounts, unmounts, and remounts to surface missing cleanup. Strict Mode in development intentionally double-invokes effects to reveal missing cleanup. With correct cleanup it is harmless and does not happen in production.

Which dependency array makes an effect re-run only when count changes?

  • []
  • [count]
  • (omitted)
  • [count, count]

Answer: [count]. [count] tells React to re-run the effect whenever the count value differs from its previous render value.

What is a good reason NOT to use an effect?

  • When you need to set the document title
  • When you subscribe to an external source
  • When you can compute the value during render instead
  • When you start a timer

Answer: When you can compute the value during render instead. If a value can be computed during render, do that instead of syncing it in an effect — not everything needs useEffect.

Continue this course

Frequently asked questions

What does the dependency array actually do?

It tells React WHEN to re-run the effect. After every render, React compares each value in the array to its value last render; if any changed, the effect runs again. An empty array [] means 'never re-run after the first time'.

What happens if I omit the dependency array entirely?

The effect runs after EVERY render. That's rarely what you want and often causes infinite loops if the effect also sets state.

What is the cleanup function for?

Returning a function from useEffect lets React tear down the previous effect before re-running it and when the component unmounts. Use it to clear timers, remove event listeners, and cancel subscriptions so you don't leak resources.

Why does my effect run twice in development?

React 18 Strict Mode intentionally mounts, unmounts, and remounts components in development to surface missing cleanup. If your cleanup is correct, the double-run is harmless and does not happen in production.

Related lessons