skip to content

In React, what is the difference between writing `useState(buildInitialState())` and `useState(buildInitialState)`, and when does that difference matter?

level: middleimportance: should knowfreq 52%

answer

  1. the argument only matters once
  2. parentheses decide when it runs
  3. function argument means initializer
  4. called with no arguments, at mount
  5. must be pure, may run twice

basics

~20 s

The first form calls buildInitialState on every render and throws the result away after the first. The second passes the function itself, so React calls it only during the initial render. That lazy form matters when the computation is expensive.

solid answer

~40 s

`useState(buildInitialState())` evaluates the call on **every** render, because it is just an argument expression, even though React only uses an initial value on the first render — after that the argument is ignored. `useState(buildInitialState)` passes the function itself, and React recognises it as a lazy initializer and calls it once, during the initial render, with no arguments. The result is identical; only the cost differs. I reach for the lazy form whenever the initial value is genuinely expensive to build — parsing something out of `localStorage`, seeding a large array, constructing a Map — and leave a literal like `useState(0)` alone, since there is nothing to defer. The initializer has to be pure: React may call it more than once, and does so deliberately in development to surface impurity.

code

jsx · 17 lines
jsx
import { useState } from 'react';

function buildSeed(size) {
  const map = new Map();
  for (let i = 0; i < size; i += 1) map.set(i, i * i);
  return map;
}

export default function Squares({ size }) {
  // eager: buildSeed runs on every render, result used only once
  // const [squares] = useState(buildSeed(size));

  // lazy: buildSeed runs during the initial render only
  const [squares] = useState(() => buildSeed(size));

  return <p>{squares.get(9)}</p>;
}

go deeper

for a junior

Know that useState's argument is the initial value and that it only applies on the first render. Recognise the arrow-function form when you see it in code.

for a middle

Explain why the eager form still executes on every render and what React does differently when the argument is a function — called once, with no arguments, at mount.

for a senior

Judge when the lazy form earns its keep versus when it is noise, and enforce initializer purity, since React can invoke it more than once and development mode double-invokes deliberately.

for a principal

Position it correctly in the cost picture: it defers a one-off render-path cost and nothing more — not caching, not memoization — so it never substitutes for fixing an expensive render or moving work off the client.

## What React does with the argument `useState` uses its argument only on the component's **initial** render. From the second render onward React already holds a value for that state, and whatever you pass is ignored. This is the fact everything else follows from, and it is worth saying out loud in an interview, because it is also why passing a prop as the initial value does not keep the state in sync with that prop later. ## The eager form pays on every render ```jsx const [draft, setDraft] = useState(JSON.parse(localStorage.getItem('draft') ?? 'null')); ``` JavaScript evaluates arguments before the call, so `localStorage.getItem` plus `JSON.parse` runs on **every** render of this component. On the first render the result becomes the state; on all later renders React discards it. Nothing is broken — the state is correct — but you are paying a synchronous cost, in the render path, forever, for a value used once. With a small literal (`useState(0)`, `useState('')`) this is free and the eager form is the right thing to write. ## The lazy form defers it ```jsx const [draft, setDraft] = useState(() => JSON.parse(localStorage.getItem('draft') ?? 'null')); ``` When the argument is a function, React treats it as an **initializer** rather than as the value. It calls it during the initial render, with no arguments, and stores what it returns. On later renders the arrow function is still allocated — that allocation is trivial — but the body never runs. Note the shape: you pass `() => expensive()` or, when the function already takes no arguments, `expensive` itself. `useState(expensive())` with parentheses is the eager form again; the missing parentheses are the entire difference, which is exactly why interviewers like this question. ## The initializer's contract - It receives **no arguments**. If you need inputs, close over them: `useState(() => seedFrom(rows))`. - It must be **pure**: compute and return a value, nothing else. React may call it more than once, and in development it deliberately invokes initializers twice so impure ones (incrementing a counter, pushing to a module array, issuing a request) show themselves immediately. - It runs during render, so it must be synchronous. There is no async initializer; a promise would simply become the state value. - Reading `window` or `localStorage` inside it still runs on the server during server rendering, so a browser-only initializer needs the usual guard or belongs in an effect. ## When it is worth using Use the lazy form when the initial value is expensive relative to a render: parsing or deserialising, reading and validating persisted state, building a `Map` or `Set` from a sizeable array, generating a large seeded structure. Use the plain form for literals and cheap expressions — wrapping `useState(0)` in an arrow adds noise and buys nothing. A useful way to frame it: the lazy initializer is about **when the work runs**, not about memoization. It does not cache anything across mounts, it does not re-run when inputs change, and it is not a substitute for computing derived values during render. ## The trap on the other side Because a function argument *means* "initializer", you cannot store a function as state by passing it directly — `useState(myCallback)` calls `myCallback` and stores its return value. To hold a function you wrap it: `useState(() => myCallback)`. The same overload exists on the setter, which is why storing a function later needs `setX(() => nextFn)` too. ## Answering compactly "The argument is only used on the first render, but with parentheses it is computed on every render and thrown away. Passing the function instead makes React call it once at mount. I use it when building the initial value is expensive, and I keep the initializer pure because React can call it more than once."

  • If the initializer only runs at mount, what happens when the value it was built from changes later?
    Nothing — React keeps the state it already has and ignores the argument on every later render. The initial value is genuinely initial, so a component that must track a changing input needs a different approach; treating useState's argument as a live default is one of the most common sources of stale UI.
  • What arguments does React pass to the initializer function?
    None. It is invoked with no arguments during the initial render, so anything it needs must be captured from the surrounding scope, as in `useState(() => parse(raw))`. That also means you cannot pass a function like `Number` and expect it to receive something useful — it would be called bare.
  • Does the lazy form avoid allocating anything on later renders?
    No — the arrow function itself is still created on every render; only its body is skipped. That allocation is negligible next to the work you deferred, which is why the pattern is worth it for expensive initial values and pure noise for a literal like useState(0).

saying these in an interview costs you the question

  • Thinks the initial value is re-applied whenever the argument changes
  • Says useState(expensive()) only runs the call once
  • Believes the initializer receives the previous state as an argument
  • Wraps every useState in an arrow function as a performance habit
  • Puts a fetch or a side effect inside the initializer

context