skip to content

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

level: juniorimportance: must knowfreq 80%

answer

  1. three shapes, three cadences
  2. no array means every render
  3. empty array is a promise, not a switch
  4. positional comparison, Object.is per entry
  5. cleanup undoes the last run

basics

~20 s

Omitting the second argument runs the effect after every render. An empty array runs it once after mount, with cleanup at unmount. A list of values re-runs it whenever any listed value changes, compared with Object.is.

solid answer

~50 s

The second argument is how you tell React *when* the effect must be synchronized again. With no array at all, React re-runs the effect after every completed render — it cannot know what the effect depends on, so it assumes everything. With `[]` you are claiming the effect depends on nothing reactive, so React runs it once after the first render and runs its cleanup only at unmount. With `[a, b]` React compares each entry against the previous render's entry with `Object.is`, position by position; if any differs, it runs the cleanup from the last run and then the effect body again. The important framing is that the array is a correctness contract, not a performance knob: it must list every reactive value the body reads, or the body will run with values frozen at the render that created it.

go deeper

for a junior

Be able to state the three cadences plainly: no array means after every render, [] means once after mount, and a list means whenever one of those values changes. Say that the returned function cleans up before each re-run and at unmount.

for a middle

Explain the comparison itself: React stores the previous array and checks each entry with Object.is, position by position, which is why a freshly created object or function counts as changed and why the array must keep a constant size.

for a senior

Show that you derive the array from the effect body rather than tuning it. Explain what breaks in production when a dependency is omitted — an effect quietly operating on values frozen at an earlier render — and how you would spot that in review.

for a principal

Frame the array as an API for declaring what a component is synchronized with, and set the team convention: dependencies are computed from the body, loops are fixed by restructuring the code, and lint suppressions need a written justification.

## The three shapes `useEffect` takes a function and an optional array. The array is the only thing that varies, and it produces three distinct cadences. ```js useEffect(() => { /* after EVERY render */ }); useEffect(() => { /* after the first render only */ }, []); useEffect(() => { /* after the first render, and whenever a or b changes */ }, [a, b]); ``` All three run *after* React has committed the render and the browser has painted. The array does not change when the effect runs relative to painting; it changes how often React decides the effect needs to run at all. ## No array: run after every render Without a second argument, React has no information about what the effect reads, so it re-synchronizes after every render — including renders caused by a parent re-rendering with identical props. This is always *correct* (the body always sees the newest values) and often wasteful. It is also the classic infinite-loop shape: if the effect calls a state setter with a new value, that render triggers the effect, which triggers a render, forever. ## Empty array: a claim, not a switch `[]` is frequently described as "run once on mount". That is what it *does*, but reading it as a switch is what produces bugs. What you are actually asserting is: *nothing this effect reads is reactive*. React takes you at your word and never re-runs the body, so the function keeps the props and state values it captured during the first render, forever. If the body reads `count`, `userId`, or a prop, those values are frozen at their first-render values and the effect silently operates on stale data. When the claim is true — attaching a listener to `window`, starting a one-time imperative setup — `[]` is exactly right. ## A list of dependencies: positional Object.is With `[a, b]`, React stores the array from the previous run and compares it entry by entry using `Object.is` — the same comparison React uses to decide whether state actually changed. Two consequences follow. First, the comparison is by identity for objects, arrays and functions: a value that is *structurally* the same but freshly created counts as changed. Second, the comparison is positional, so the array must have the same length and the same order on every render. Writing a dependency array whose contents depend on a condition breaks that: ```js // wrong: the array's size changes between renders useEffect(() => { /* ... */ }, isAdmin ? [userId, role] : [userId]); ``` In development React warns that the final argument changed size between renders and that the order and size must remain constant. The array should be a literal with a fixed shape. ## Cleanup cadence follows the array The function you return is not "the unmount handler"; it is "undo the last run". React runs it before each re-run of the effect and once more at unmount. So the cadence of cleanup is exactly the cadence of the array: every render with no array, before each change with a dependency list, only at unmount with `[]`. A subscription started in an effect with `[roomId]` is torn down and re-established each time `roomId` changes, which is usually precisely what you want. ## Choosing between them You do not really choose. You write the effect body, then list every reactive value it reads — props, state, and anything derived from them inside the component — and the array falls out of that. The `react-hooks/exhaustive-deps` lint rule computes the same list mechanically. If the resulting array causes the effect to run more often than you want, the fix is to change the *code* (move a value inside the effect, use a functional state updater, stabilize an identity with `useMemo` or `useCallback`) rather than to trim the array. Trimming the array does not make the dependency go away; it only hides it from React. ## What an interviewer is listening for The weak answer treats the array as an optimization: "pass `[]` so it doesn't run so much." The strong answer describes it as a declaration of what the effect is synchronized with, notes `Object.is` and positional comparison, and can state precisely what breaks when the declaration is a lie — the body keeps running against values captured at an earlier render.

  • Does an effect with an empty dependency array see current state if the component re-renders later?
    No. The body ran once, during the first render, and the function it created closed over that render's props and state. Those values never update, because React never calls the function again. If the effect needs current values, either list them as dependencies, read them through a ref, or use a functional state updater so the value is supplied by React at update time rather than captured.
  • What happens if a dependency array has a different number of entries on two consecutive renders?
    React compares dependencies positionally, so it cannot pair them up. In development it warns that the final argument passed to useEffect changed size between renders and that the order and size must remain constant, and it re-runs the effect. The fix is to make the array a fixed-shape literal and move any conditional logic inside the effect body.
  • Is omitting the dependency array ever the right choice?
    Rarely, and usually only briefly. It is defensible for an effect that genuinely must re-synchronize on every commit, such as one that measures or reports something about the latest rendered output. In almost every other case it does redundant work and risks a render loop if the effect sets state, so a real dependency list is the better answer.

saying these in an interview costs you the question

  • Calling the array a performance optimization rather than a correctness contract
  • Saying an empty array means the effect always sees current state
  • Believing the returned function only runs at unmount
  • Thinking React deep-compares the dependency values
  • Treating a conditional dependency array as valid

context