Skip to the solution
MediumFrontend7 min read

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

1.1k views

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.

  • hooks
  • usestate
  • usecallback
  • dependencies

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe object-in-deps loop, and three ways to fix it
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 29 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

Describe the `useRef` hook and its typical use cases.

Continue