Skip to solution
mediumFrontend

What is the purpose of the `deps` array in `useEffect` and `useCallback`?

1.1k views
01

Understand the problem

Question presented to candidate: "What is the dependency array actually for, and why do people get it wrong so often?"

What a strong answer should cover:

  • It declares which reactive values the hook depends on. React compares them with Object.is and only re-runs (or re-creates) when one changed.
  • Three cases: omitted (runs every render), [] (once on mount), [a, b] (when a or b change).
  • The comparison is by identity, not by value. Two structurally identical objects are different dependencies.
  • That is why an inline object, array, or function in deps re-runs the hook on every render — the classic infinite-loop bug when the effect also sets state.
  • The fix hierarchy: depend on primitives where possible; otherwise stabilise with useMemo/useCallback; or move the value inside the effect.
  • Do not lie to the linter. An omitted dependency means the hook closes over a stale value — the cause of most stale-closure bugs.
  • useCallback and useMemo use the same array for the same reason: to decide whether to return the cached value or make a new one.
  • useEffectEvent as the modern answer for logic that should read the latest value without being reactive.

Clarifying questions expected:

  • "Is the dependency a primitive or an object?" — it changes the whole answer.
  • "Is the effect setting state that feeds back into its own dependencies?"

Code / implementation expected: Yes — the object-literal-in-deps bug and its fix.

hooksusestateusecallbackdependencies
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes useEffect basics. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every count in sections 3 and 5 was measured by rendering the compo

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The object-in-deps loop, and three ways to fix it
Run Playground
import { useState, useEffect, useMemo, useCallback, useRef } from "react";

// Counters live outside so a re-render cannot reset them.
const runs = { broken: 0, memoised: 0, primitive: 0, inside: 0 };

function Demo({ userId, tick }) {
  const renders = useRef(0);
  renders.current++;

  // ❌ BROKEN: a new object every render, so Object.is always fails.
  // If this effect called setState it would loop forever.
  const brokenQuery = { userId, limit: 10 };
  useEffect(() => { runs.broken++; }, [brokenQuery]);

  // ✅ FIX 1: stabilise the object with useMemo.
  const memoisedQuery = useMemo(() => ({ userId, limit: 10 }), [userId]);
  useEffect(() => { runs.memoised++; }, [memoisedQuery]);

  // ✅ FIX 2 (best): depend on the primitive. Strings and numbers compare
  // by value, so the whole class of problem disappears.
  useEffect(() => { runs.primitive++; }, [userId]);

  // ✅ FIX 3: build the object INSIDE the effect, so it is not a dependency.
  useEffect(() => {
    const query = { userId, limit: 10 };
    runs.inside++;
    void query;
  }, [userId]);

  const row = { padding: "3px 12px 3px 0", fontFamily: "ui-monospace, monospace", fontSize: 13 };

  return (
    <table style={{ borderCollapse: "collapse" }}>
      <tbody>
        <tr><td style={row}>component renders</td><td style={row}><strong>{renders.current}</strong></td></tr>
        <tr><td style={{ ...row, color: "#a33" }}>deps [object literal]</td><td style={{ ...row, color: "#a33" }}>{runs.broken}</td></tr>
        <tr><td style={{ ...row, color: "#161" }}>deps [useMemo object]</td><td style={{ ...row, color: "#161" }}>{runs.memoised}</td></tr>
        <tr><td style={{ ...row, color: "#161" }}>deps [userId] primitive</td><td style={{ ...row, color: "#161" }}>{runs.primitive}</td></tr>
        <tr><td style={{ ...row, color: "#161" }}>object built inside</td><td style={{ ...row, color: "#161" }}>{runs.inside}</td></tr>
      </tbody>
    </table>
  );
}

export default function App() {
  const [tick, setTick] = useState(0);
  const [userId, setUserId] = useState(1);
  const [count, setCount] = useState(0);

  // The functional updater reads the latest value from React, so count does
  // not need to be in the deps array of anything that increments it.
  const incTwice = useCallback(() => {
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  }, []);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <Demo userId={userId} tick={tick} />
      <p style={{ marginTop: 12 }}>
        <button onClick={() => setTick((t) => t + 1)}>Re-render ({tick})</button>{" "}
        <button onClick={() => setUserId((u) => (u === 1 ? 2 : 1))}>Change userId (now {userId})</button>{" "}
        <button onClick={incTwice}>+2 via functional updater ({count})</button>
      </p>
      <p style={{ color: "#666", fontSize: 13 }}>
        Press Re-render repeatedly: only the first effect climbs, because its
        dependency is a brand-new object each time. The other three stay put
        until userId actually changes.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 29 of 119 decoded in the React.js track. One more won't hurt.

Back to track