A React component must show a loading spinner, an error message, or a data table depending on its state. How do you structure that branching in JSX, and why are deeply nested ternaries discouraged?
answer
- braces take a value, not a statement
- statement land lives above the return
- one guard clause per state
- hooks first, then the guards
- two-way ternary, never three
basics
~20 sUse early returns above the JSX, one per state, so each branch reads as a flat guard clause. JSX braces hold expressions, not statements, so an inline if is impossible; nested ternaries are the workaround people reach for, and they scale badly in review and in diffs.
solid answer
~50 sFor mutually exclusive states I use **early returns** — `if (status === 'loading') return <Spinner />;`, then error, then the empty case, and the happy path last. Above the returned JSX you are in statement land, so ordinary `if` works and each branch stays flat and named. Inside JSX you only have expressions, which is why an inline `if` is not an option and why people stack ternaries; a two-way ternary is fine, but three deep becomes unreadable and produces diffs where a one-line change re-indents every branch. The other good shapes are extracting the branch into a small component or a function returning an element, and — for a state enum — a lookup object mapping each state to its element. One practical constraint: every hook call has to run before any early return, so the guards go after the hooks, not before them.
code
jsx · 16 linesfunction Spinner() {
return <p>Loading…</p>;
}
function Report({ status, rows }) {
if (status === 'loading') return <Spinner />;
if (status === 'error') return <p role="alert">Failed to load</p>;
if (rows.length === 0) return null;
return (
<ul>
{rows.map((row) => (
<li key={row.id}>{row.label}</li>
))}
</ul>
);
}go deeper
Know that braces take an expression, so if cannot go inside JSX, and that returning early with return null is the normal way to render nothing.
Name the shapes and when each applies: early returns for exclusive states, a variable or extracted component when the branches share a wrapper, a boolean guard for one-sided cases. Explain that nested ternaries are a readability and diff problem, not a correctness one.
Argue the tradeoff out loud — extraction buys a name and a test surface at the cost of an indirection — and connect the shape to the hook ordering constraint so the file reads as hooks, guards, JSX.
Own it as a convention question: pick one branching shape as the team default so components stay diff-friendly and reviewable, and treat a component that both lays out and dispatches on state as a candidate for splitting along that seam.
## Why the question exists Every non-trivial component has more than one thing it might render, and JSX gives you no branching construct of its own. What you can do is entirely determined by one fact — braces embed an **expression** — plus your freedom to write ordinary JavaScript before the `return`. Interviewers ask this because the naive answer (nest ternaries until it works) is what junk code looks like, and the good answers reveal whether you think about the shape of the component, not just the output. ## Expressions, not statements ```jsx // not possible: if is a statement <div>{ if (loading) { ... } }</div> ``` Braces are substituted with the *value* of what is inside them, so anything that has no value — `if`, `for`, `switch`, a variable declaration — cannot go there. What does have a value: a ternary, `&&`, a function call, a variable you assigned earlier, an array literal. That leaves four workable shapes. ## Shape 1: early returns (the default for exclusive states) ```jsx function Report({ status, rows }) { if (status === 'loading') return <Spinner />; if (status === 'error') return <ErrorNote />; if (rows.length === 0) return null; return <Table rows={rows} />; } ``` Each state gets one line at the same indentation level. Adding a fifth state adds one line and touches nothing else, which is what makes it review-friendly. `return null` is the idiomatic way to render nothing, because `null` is one of the values React skips. The constraint: hooks must be called unconditionally and in the same order on every render, so all hook calls sit **above** the guards. In practice that means hooks first, guards second, main JSX last — a shape worth adopting as a house rule precisely because it makes the constraint visible. ## Shape 2: a variable assigned before the return ```jsx let body; if (status === 'loading') body = <Spinner />; else if (status === 'error') body = <ErrorNote />; else body = <Table rows={rows} />; return <Card title="Report">{body}</Card>; ``` This is the shape to use when the branches share a wrapper. Early returns would force you to repeat the `<Card>` in every branch; hoisting the varying part into a variable keeps the frame written once. Elements are ordinary values, so storing one in a variable costs nothing. ## Shape 3: extract a component or a function When a branch is more than a line or two, give it a name: ```jsx function ReportBody({ status, rows }) { /* the guards live here */ } return <Card title="Report"><ReportBody status={status} rows={rows} /></Card>; ``` The branching has not gone away; it now has a name and its own test surface. This is usually the right move once the parent is doing layout *and* state dispatch, because the two change for different reasons. ## Shape 4: a lookup for a state enum ```jsx const views = { idle: <Placeholder />, loading: <Spinner />, error: <ErrorNote />, }; return views[status] ?? <Table rows={rows} />; ``` Worth reaching for when the states form a closed set and the branches are one-liners. Note that a plain object literal builds every branch's element eagerly, so use a map of functions if a branch is expensive to construct, and keep a fallback for an unexpected value. ## What is actually wrong with nested ternaries A single ternary is a fine, idiomatic two-way choice. The problems start at the second level: - **Readability.** `a ? x : b ? y : z` has no visual marker of where a branch begins; the reader has to parse associativity to know which `:` pairs with which `?`. - **Diffs.** Adding one branch re-indents the whole chain, so a one-idea change shows up as a large diff and the real edit hides in it. - **Debuggability.** You cannot put a breakpoint or a log line on a branch of an expression without restructuring it first. - **Ordering bugs.** The chain encodes precedence implicitly; a mis-ordered pair silently makes a later branch unreachable, and nothing flags it. None of these are correctness problems — nested ternaries produce the right output. That is the point worth making in an interview: you are choosing between structures that all work, on maintainability grounds, and you should be able to say what changes when the branch count grows. ## The two-line answer to give Exclusive states get early returns; a varying region inside a fixed frame gets a variable or an extracted component; a one-sided "show this or nothing" gets a boolean guard. Ternaries stay two-way. That covers essentially every real component.
- When would you keep the branches inline in the JSX rather than using early returns?When the branches share a wrapper. Early returns force you to repeat the frame in every branch, so once there is a `<Card>` or a layout shell around the varying part, I assign the varying element to a variable before the return, or extract it into its own component, and keep the frame written once.
- Is a ternary ever the right choice here?Yes, for a genuine two-way choice — `{isEditing ? <Editor /> : <Preview />}` is clearer than anything else. The rule I apply is one level only: as soon as a branch of a ternary is itself a ternary, the code wants a different structure, usually early returns or an extracted component.
- What breaks if you put an early return above your hook calls?The component would call a different number of hooks depending on the branch taken, which React does not allow — hooks must run unconditionally, in the same order, on every render. The practical shape is hooks first, guard clauses second, main JSX last, so the constraint is visible in the file's layout.
saying these in an interview costs you the question
- Claims you can write an if inside JSX braces
- Treats nested ternaries as producing wrong output
- Wraps branches in an IIFE as the default pattern
- Puts early returns above the hook calls
- Duplicates the shared wrapper across every branch