Lesson 68 of 80
useState(expensiveWork()) runs expensiveWork() on every single render, even though only the first one keeps the result — passing a function instead fixes it.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
useState's argument is only ever used on a component's first render — every render after that, React already has the value in its slot and ignores whatever you pass in. But "ignores" doesn't mean "doesn't evaluate." If that argument is a function call — useState(buildInitialData()) — JavaScript evaluates buildInitialData() before useState ever sees the result, on every render, and then React throws the result away on every render but the first.
useState has a second calling form specifically to fix this: pass the function itself, not its result. React calls it exactly once, only on the first render, and never touches it again.
function parseHugeConfig(raw) {
console.log("parsing..."); // watch how often this actually runs
return JSON.parse(raw);
}
function Settings({ rawConfig }) {
// wrong: parseHugeConfig runs on EVERY render — result discarded on all but the first
const [config, setConfig] = useState(parseHugeConfig(rawConfig));
// right: parseHugeConfig runs ONCE, ever — only on the first render
const [config2, setConfig2] = useState(() => parseHugeConfig(rawConfig));
return <button onClick={() => setConfig2({ ...config2, saved: true })}>Save</button>;
}
Every time Settings re-renders — a parent re-rendering it, a sibling state update, anything — the first form re-parses rawConfig and immediately discards the result. The second form only calls parseHugeConfig the one time its result is actually kept. For a cheap default like useState(0) this difference is unmeasurable; for reading from localStorage, parsing a large payload, or building an initial derived structure from props, it's real, repeated, wasted work on every re-render.
useMemo. useMemo recomputes whenever its dependency array changes — it's for a value that's derived and needs to stay in sync with something. Lazy useState init runs exactly once, ever, for the component instance's entire lifetime, dependency array or not — it's for a value that only ever needs to be computed once, even though it can still be updated afterward via the setter.key, for instance) resets this too. "Only once" means once per mount, not once ever globally — a remounted instance runs the initializer function again, exactly as if it were a brand-new component instance, because as far as React is concerned, it is one.This is a direct extension of how useState itself works elsewhere in this track — a closure standing in for React's internal state slot. What's new here is only when the value passed to that closure gets evaluated: eagerly, every render, if you hand it a value or a function call; lazily, exactly once, if you hand it the function itself and let React decide when to call it.