Conditional rendering
State gave us the first moving piece. A component can render with one value, an event can change state, and React can render the component again with a new value.
That creates the next hole:
if a later render can see a different value, how does the component return a different piece of UI?
Start with the smallest version. A status component receives a boolean. When the value is true, it should show one label. When the value is false, it should show another. The obvious JavaScript tool is if, so the first attempt is to put if exactly where the label should appear:
function Status({ isOnline }) {
return (
<div>
{if (isOnline) {
'Online';
} else {
'Offline';
}}
</div>
);
}
root.render(<Status isOnline={true} />);
Waiting to run
Not run yet.
That fails before React can render anything. The reason is the same constraint we saw in JSX itself: { ... } is an expression hole, not a statement block. An if...else is a statement[2], so it cannot sit directly inside that expression position.
So the first attempt gives us a precise constraint:
JSX can contain JavaScript expressions, but not JavaScript statements.
The next attempt has to keep the if, but move it somewhere statements are allowed.
Branch before returning #
A component body is ordinary JavaScript. Statements are allowed before return, so the smallest corrected version is:
function Status({ isOnline }) {
if (isOnline) {
return <div>Online</div>;
}
return <div>Offline</div>;
}
root.render(<Status isOnline={true} />);
Waiting to run
Not run yet.
This works because the component chooses the React element before handing anything back to React. Nothing special happened to the JSX. The component received props, JavaScript checked a value, and the component returned one element description or another.
If the input changes, the same function chooses the other branch:
function Status({ isOnline }) {
if (isOnline) {
return <div>Online</div>;
}
return <div>Offline</div>;
}
root.render(<Status isOnline={false} />);
Waiting to run
Not run yet.
That gives us the first working rule:
when the whole returned shape changes, use ordinary JavaScript control flow before
return
But this answer is larger than we need for every case. Sometimes the outer UI is stable, and only one position inside it changes.
function AccountPanel({ isSignedIn }) {
if (isSignedIn) {
return (
<section>
<h2>Account</h2>
<button>Open settings</button>
</section>
);
}
return (
<section>
<h2>Account</h2>
<button>Sign in</button>
</section>
);
}
root.render(<AccountPanel isSignedIn={false} />);
Waiting to run
Not run yet.
This works, but it exposes a new hole in the shape of the code. The <section> and <h2> are not conditional, but we had to write them twice just to choose one button.
So the next question is narrower:
how do we choose one value inside JSX without duplicating the stable wrapper?
Conditional expressions #
We already know an if statement cannot go into the JSX expression hole. So the replacement has to be an expression. The conditional operator is an expression that chooses one of two values[3]:
function AccountPanel({ isSignedIn }) {
return (
<section>
<h2>Account</h2>
{isSignedIn ? (
<button>Open settings</button>
) : (
<button>Sign in</button>
)}
</section>
);
}
root.render(<AccountPanel isSignedIn={false} />);
Waiting to run
Not run yet.
Now the stable wrapper is written once. Only the changing position contains the branch.
That gives us the next rule:
when one position inside JSX needs one of two values, use an expression that produces one of those values
The two values do not have to be large elements. The same idea works for text and props:
function SaveButton({ isSaving }) {
return (
<button disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</button>
);
}
root.render(<SaveButton isSaving={true} />);
Waiting to run
Not run yet.
Here the same condition chooses a prop value and a text child.
Naming the chosen value #
The conditional operator solves the expression-hole problem, but it can become hard to read when the branches grow.
Here is the pressure:
function Toolbar({ canEdit }) {
return (
<section>
<h2>Document</h2>
{canEdit ? (
<div>
<button>Edit document</button>
<button>Share draft</button>
</div>
) : (
<span>Read only</span>
)}
</section>
);
}
root.render(<Toolbar canEdit={true} />);
Waiting to run
Not run yet.
This is still valid. The hole is that the returned tree is now harder to see because the branching detail is sitting inside it. The same constraint that helped earlier helps again: the component body is ordinary JavaScript. We can choose the branch first, give it a name, and then insert the chosen value:
function Toolbar({ canEdit }) {
let action;
if (canEdit) {
action = <button>Edit document</button>;
} else {
action = <span>Read only</span>;
}
return (
<section>
<h2>Document</h2>
{action}
</section>
);
}
root.render(<Toolbar canEdit={true} />);
Waiting to run
Not run yet.
The variable is just a JavaScript variable holding the React node selected for this render. So the rule becomes:
when the branch is too large to read comfortably inside JSX, choose it before the
return
Rendering nothing #
Now another hole appears. What if one branch should not produce an alternative element at all? A notice is either visible or absent:
function Notice({ visible }) {
if (!visible) {
return null;
}
return <p>Profile saved</p>;
}
root.render(<Notice visible={false} />);
Waiting to run
Not run yet.
The component still ran. It still returned a value. That value is null, and React treats it as "render no UI here"[1].
The same null value also fits inside JSX:
const { useState } = React;
function NoticeToggle() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible((current) => !current)}>
Toggle notice
</button>
{visible ? <p>Profile saved</p> : null}
</div>
);
}
root.render(<NoticeToggle />);
setTimeout(() => {
mountNode.querySelector('button').click();
}, 20);
Waiting to run
Not run yet.
The first render has no paragraph. The click changes state. The next render evaluates the condition again and returns a tree that includes the paragraph. So null fills the "nothing here" hole:
a branch can produce
nullwhen it should contribute no visible UI
Optional pieces #
The ternary version works, but optional UI often has a lopsided shape:
{visible ? <p>Profile saved</p> : null}
One branch has the actual UI. The other branch only says "nothing". The logical AND operator can express that lopsided shape in one expression[4]:
function Inbox({ unreadCount }) {
return (
<section>
<h2>Inbox</h2>
{unreadCount > 0 && (
<p>{unreadCount} unread messages</p>
)}
</section>
);
}
root.render(<Inbox unreadCount={3} />);
Waiting to run
Not run yet.
This works because condition && value evaluates to value when the condition is truthy. When the left side is the boolean false, React receives a false value in that child position, which does not create visible output.
But this shortcut has its own hole. The left side is not automatically converted to a boolean. If the left side is the number 0, the expression produces 0, and React can render numbers as text:
function Inbox({ unreadCount }) {
return (
<section>
<h2>Inbox</h2>
{unreadCount && (
<p>{unreadCount} unread messages</p>
)}
</section>
);
}
root.render(<Inbox unreadCount={0} />);
Waiting to run
Not run yet.
The component wanted to render no message. Instead, the child position received the number 0. That is why the previous version used unreadCount > 0:
use
&&for optional UI, but make the left side a boolean condition
Conditions belong to each render #
So far, props have been enough to show the branch selection. State makes the timing visible. The condition is evaluated each time React calls the component:
const { useState } = React;
function Details() {
const [open, setOpen] = useState(false);
return (
<section>
<button
aria-expanded={open}
onClick={() => setOpen((current) => !current)}
>
{open ? 'Hide details' : 'Show details'}
</button>
{open && <p>Conditional rendering happens during render.</p>}
</section>
);
}
root.render(<Details />);
setTimeout(() => {
const button = mountNode.querySelector('button');
button.click();
setTimeout(() => {
const section = mountNode.querySelector('section');
console.log('section text:', section.textContent);
}, 0);
}, 20);
Waiting to run
Not run yet.
The first render sees open === false, so the paragraph is absent. The click calls the state setter. React renders again, and this time the same JavaScript condition sees open === true.
That keeps the model consistent with state:
- a render sees one snapshot of state
- JavaScript branches using that snapshot
- the component returns a description for that branch
- a later state update causes a later render
- the later render may choose a different branch
Filling the hole #
We started with a component that needed to show different UI for different values. The first attempt put if in the JSX expression hole, and that failed because statements do not fit there.
Every later shape came from that constraint:
- if the whole returned shape changes, branch before
return - if one position inside JSX changes, use an expression such as a ternary
- if the branch is too large for inline JSX, choose it in a variable first
- if one branch should show nothing, use
null - if optional UI is just
thing or nothing,&&can express it when the left side is boolean
Conditional rendering chooses among the values we already know:
- elements, such as
<button>Save</button> - text, such as
'Saving...' - prop values, such as the boolean in
disabled={isSaving} null, when a component or child position should render nothing
The choice happens inside the component function while it is building the description for this render.
Final definition #
Conditional rendering is ordinary JavaScript control flow used inside a component to choose which React nodes, text, prop values, or null values become part of the returned description for the current props and state.
Summary #
Conditional rendering fills the hole after state:
- JSX expression holes cannot contain
ifstatements directly - a component can use
if...elsebefore returning - returning
nullmeans this branch renders no visible UI - ternaries choose between two values inside JSX
&&is useful for optional UI when the left side is a boolean condition- each render evaluates conditions from that render's props and state
Notes
- The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development modules. Examples focus on the author-facing shape of conditional rendering rather than React's internal update strategy.