skip to content

A React component must render nothing when its `user` prop is missing, but it also needs useState and useEffect. How do you satisfy the Rules of Hooks without running the effect's work for a missing user?

level: middleimportance: must knowfreq 62%

answer

  1. the return is not the problem, its position is
  2. branch inside the callback, not around it
  3. push the condition to the parent
  4. unmounting also clears the state
  5. short-circuit is a conditional call too

basics

~20 s

Put every hook call above the early return and branch inside the effect body, or lift the condition into the parent so the child component only mounts when a user exists. Never move a hook below the return.

solid answer

~40 s

Two shapes, and I would pick between them. The quick one keeps all hook calls at the top of the component, unconditional, and guards *inside* the effect: `useEffect(() => { if (!user) return; subscribe(user.id); }, [user])`, with `if (!user) return null;` placed after the hooks. That obeys the rule because the calls still run every render — only the work inside them is conditional. The cleaner one moves the branch up a level: the parent renders `{user ? <UserPanel user={user} /> : null}` and `UserPanel` takes a non-nullable `user`, so it never has to deal with the empty case at all. I prefer the second when the component's state is meaningless without the user, because unmounting also throws that state away instead of leaving stale values around for the next user.

go deeper

for a junior

Remember the ordering: all hook calls first, early returns after. Putting a hook below if (!user) return null; is the mistake to recognise on sight.

for a middle

Explain that the constraint is a stable call count, then show both fixes — guard inside the effect, or lift the condition into the parent — and note that cond && useHook() breaks the same way.

for a senior

Argue the choice on lifecycle grounds: lifting the branch unmounts the child, running cleanup and discarding state, which is usually what you want when the prop is the component's whole reason to exist.

for a principal

Frame it as a props-modelling problem: a nullable prop that every branch inside the component has to re-check is a design smell, and making it non-nullable at the boundary removes a whole class of hook-placement bugs from the codebase.

## Why the obvious fix is illegal The instinct is to bail out first and set up state afterwards: ```jsx function UserPanel({ user }) { if (!user) return null; const [tab, setTab] = useState('overview'); // illegal useEffect(() => { subscribe(user.id); }, [user.id]); // illegal } ``` On renders where `user` is missing this component makes zero hook calls; on renders where it exists it makes two. React matches hooks to their stored records by call order, so a changing call count corrupts the mapping — and when the count drops it throws `Rendered fewer hooks than expected.` The rule is not "don't return early", it is "don't let a hook call be skipped". ## Shape 1: hooks first, guard inside Move every hook above the return and push the condition into the callback: ```jsx function UserPanel({ user }) { const [tab, setTab] = useState('overview'); useEffect(() => { if (!user) return; const sub = subscribe(user.id); return () => sub.close(); }, [user]); if (!user) return null; return <Tabs value={tab} onChange={setTab} />; } ``` Every render calls `useState` once and `useEffect` once, so the slot mapping is stable. The effect still does no work while `user` is null, because the guard is inside the callback where control flow is unrestricted. The early return itself is completely fine — it is only its *position* relative to the hooks that mattered. This shape has two costs worth naming. The component now pays for state it may not use, which is trivial. More importantly, the state survives the empty period: if `user` becomes null and then a *different* user arrives, `tab` is still whatever the previous user selected, because the component never unmounted. ## Shape 2: lift the condition to the parent ```jsx function Page({ user }) { return user ? <UserPanel user={user} /> : <EmptyState />; } function UserPanel({ user }) { // user is always present here const [tab, setTab] = useState('overview'); useEffect(() => { const sub = subscribe(user.id); return () => sub.close(); }, [user.id]); return <Tabs value={tab} onChange={setTab} />; } ``` Now the Rules of Hooks question disappears rather than being worked around. `UserPanel` has one job, its props type has no nullable field, and there is no `if (!user)` scattered through its body and its callbacks. When `user` goes away the component unmounts: the effect's cleanup runs and the state is discarded, so the stale-tab problem from shape 1 cannot occur. This is the version to reach for when the component is genuinely meaningless without the value, which is most of the time. A useful signal: if you find yourself writing `user?.` or `if (!user)` more than once inside the body, the branch belongs one level up. ## Which one in an interview Say both, then justify the choice on lifecycle grounds rather than on style. The interviewer is checking that you understand *why* the hook placement is constrained — that call count, not source-code tidiness, is the constraint — and that you know the branch can live in three places: inside the callback, above the hooks in a parent, or nowhere at all if the prop can be made non-nullable upstream. ## The variant that still trips people A conditional hook hidden behind `&&` or a ternary is the same defect in a smaller font: ```jsx const theme = user && useContext(ThemeContext); // illegal ``` The `react-hooks/rules-of-hooks` rule from `eslint-plugin-react-hooks` reports this alongside the `if` form, because short-circuiting means the call sometimes does not happen. Note that React 19's `use` is the deliberate exception to the conditional-call restriction; the classic hooks are not. Also worth knowing: loading states are the most common source of this bug. `if (isLoading) return <Spinner />;` placed above the hooks looks harmless and breaks the same way — the fix is identical, hooks first, returns after.

  • Instead of guarding inside the effect, why not pass a dependency array that makes it skip — say `[user ? user.id : null]`?
    That controls *when* the effect re-runs, not whether it runs at all. On mount the effect always runs once regardless of its deps, so a null user would still reach the body. Deps are a re-run comparison, not a conditional execution switch, so the guard has to be inside the callback.
  • Which shape would you choose if the component's state must survive the user briefly going null?
    Then hooks-first with an internal guard, because lifting the branch unmounts the component and discards its state. That is a deliberate decision, not a fallback: you are choosing to keep the state alive across the gap. If the state must survive but the component should still unmount, the state belongs in the parent instead.
  • Does an early return before the hooks ever work by accident?
    It can appear to, when the condition happens to be constant for the component's whole lifetime — the call count never changes, so nothing shifts. It is still a latent bug: the first render where the condition flips corrupts the slot mapping or throws `Rendered fewer hooks than expected.` The lint rule flags it regardless, which is the right call.

saying these in an interview costs you the question

  • Says early returns are banned in components that use hooks
  • Moves the hook into the branch and calls it a fix
  • Thinks a dependency array can stop an effect from running on mount
  • Claims `cond && useMemo(...)` is fine because it is one line
  • Believes conditional rendering by the parent is itself a hooks violation

context