skip to content

In React, what is the difference between calling useEffect(fn) with no second argument, useEffect(fn, []) with an empty array, and useEffect(fn, [a, b])?

level: juniorimportance: must knowfreq 85%

answer

  1. three forms, one comparison
  2. the array is optional, empty is not
  3. positional check against last render
  4. nothing to compare means run again
  5. empty list can never differ

basics

~20 s

The second argument controls re-runs: omitting it runs the effect after every render, an empty array runs it once after mount, and a populated array re-runs it only when one of the listed values changes.

solid answer

~40 s

`useEffect(setup, deps?)` runs `setup` after React commits the render to the DOM, and the optional second argument decides whether it runs again. With no array there is nothing to compare, so React re-runs the effect after every commit — that is the form that causes an infinite loop if the effect sets state. With `[]`, React compares an empty list to an empty list, so nothing ever differs and the effect runs once after mount, with its cleanup running at unmount. With `[a, b]`, React compares each item to the same position in the previous render's array and re-runs only when a pair differs. Worth saying out loud: "runs once" is not a special mode — it is just the ordinary comparison over a list that can never change.

code

javascript · 19 lines
javascript
import { useEffect } from 'react';

function Example({ label }) {
  useEffect(() => {
    console.log('after every render');
  });

  useEffect(() => {
    console.log('once, after mount');
  }, []);

  useEffect(() => {
    console.log('when label changes:', label);
  }, [label]);

  return null;
}

export default Example;

go deeper

for a junior

Be able to state the three forms without hesitating: no array means after every render, an empty array means once after mount, and a listed array means when one of those values changes. Say that the array is a second argument to useEffect.

for a middle

Explain that all three forms are the same positional comparison over the previous render's array, and that a missing array simply has nothing to compare. Mention the constant-size requirement and why an effect that sets state with no array loops forever.

for a senior

Show judgment about which form belongs in real code: treat an omitted array as a smell, justify an empty array by arguing the effect has no reactive inputs, and explain how an inaccurate array turns into a stale-value bug that only shows up under specific navigation.

for a principal

Be ready to argue for conventions that keep dependency arrays honest across a large codebase — where effects are allowed at all, when a value should be reshaped into a primitive dependency, and how you keep teams from silencing the problem instead of fixing the input.

## The signature React's effect hook takes up to two arguments: `useEffect(setup, dependencies?)`. `setup` is a function React calls **after** it has committed the rendered output to the DOM. `dependencies` is an optional array of values. It is optional in the literal sense: passing nothing and passing `[]` are two different calls, not two spellings of the same thing. ## What React stores between renders Each `useEffect` call owns a slot in the component's hook list, and React keeps the dependency array you passed in that slot. On the next render it takes the array you pass this time and compares it to the stored one **positionally**, item by item, with `Object.is`. If every pair matches, React skips the effect entirely — no cleanup call, no setup call. If any pair differs, or if you passed no array at all, React runs the previous cleanup (if the last setup returned one) and then runs setup again. That one mechanism explains all three forms. ## Form 1 — no second argument ```js useEffect(() => { console.log('ran'); }); ``` There is no stored array to compare against, so React re-runs the effect after **every** commit — including commits caused by a parent re-rendering with identical props. This is the form behind the classic runaway loop: if the effect calls a state setter, the state change schedules another render, that render runs the effect again, and it never settles. ## Form 2 — an empty array ```js useEffect(() => { console.log('mounted'); }, []); ``` An empty array compared against an empty array has zero pairs to check, so all pairs match vacuously and React skips the effect on every subsequent render. Setup runs once after the component mounts; the cleanup it returned runs when the component unmounts. The framing matters more than the rule. React is not offering an "on mount only" mode — it is doing the same comparison over a list that can never change. Because of that, an effect with `[]` keeps using the props and state values from the render that created it, so an empty array is a claim you are making about the effect's inputs, not a free pass. ## Form 3 — a populated array ```js useEffect(() => { document.title = `${label} (${count})`; }, [label, count]); ``` After each render React checks `Object.is(prevLabel, label)` and `Object.is(prevCount, count)`. If both hold, nothing happens. If either fails, cleanup then setup. Note that React never inspects what your setup function actually reads — it only compares the array you handed it. ## The array must keep a constant size Because the comparison is positional, the array has to have the same number of items on every render. React warns when it does not: *"The final argument passed to useEffect changed size between renders."* So `[flag ? a : b]` is fine — one slot, always — while `flag ? [a] : [a, b]` is not. For the same reason you cannot build a dependency array by spreading a list whose length varies. ```js // fine: always one slot useEffect(fn, [flag ? a : b]); // broken: the slot count depends on flag useEffect(fn, flag ? [a] : [a, b]); ``` ## Comparison is shallow, by identity `Object.is` is a reference/identity check for objects and arrays and a value check for primitives. It is not a deep comparison, so two structurally identical objects created in different renders are different dependencies. That is why primitives make far better dependencies than freshly built objects. ## The same argument on useLayoutEffect `useLayoutEffect(setup, deps?)` takes exactly the same second argument with exactly the same semantics — omitted, empty, or populated behave identically. Only *when* the setup runs relative to the browser's paint differs between the two hooks; the dependency contract is shared. ## Picking a form in practice Omitting the array is almost always a mistake rather than a choice: if the effect really should run after every render, saying so explicitly with the values it depends on is clearer and survives refactoring. Populated arrays are the normal case. Empty arrays are legitimate when the effect genuinely has no reactive inputs — wiring up something that lives for the component's whole lifetime.

  • If an effect with an empty dependency array is really "run once", why is that phrasing considered misleading?
    Because React is not running a special mount-only mode — it is comparing an empty list to an empty list and finding no difference. The effect still closes over the props and state of the render that created it, so an empty array is an assertion that the effect has no reactive inputs. If it actually reads one, the effect quietly keeps using the first render's value.
  • What happens if the number of items in a dependency array changes between renders?
    React compares the arrays by position, so a size change makes the comparison meaningless. React logs a development warning that the final argument passed to useEffect changed size between renders and falls back to re-running the effect. Keep the array a fixed-length literal and put any conditional logic inside a slot, such as `[flag ? a : b]`, rather than choosing between arrays.
  • Does React look at what the effect function actually uses when deciding whether to re-run it?
    No. React only compares the array you passed. The setup function is opaque to it, so an effect that reads a value it never listed will not re-run when that value changes, and an effect listing a value it never reads will re-run pointlessly. Keeping the array honest is the author's job, not React's.

saying these in an interview costs you the question

  • Claims omitting the array is the same as passing []
  • Says an empty array means the effect never cleans up
  • Thinks React inspects the effect body to decide re-runs
  • Believes dependencies are compared deeply, not by identity
  • Builds the dependency array conditionally with different lengths

context