React 20 ⚛️ Conditional Rendering with If Statements
Conditional rendering is the practice of choosing what to render based on the application’s state. React components are functions, and JavaScript’s control flow is available inside them. The if statement, the ternary operator, the logical && operator, and the switch statement are all valid ways to decide what a component returns. The choice between them is a matter of which construct expresses the decision most clearly, and the constraint is that the expression must produce a value that JSX can render.
The if statement is the most familiar conditional, and it is the right tool for early returns and multi-branch decisions. It cannot appear directly inside JSX, because JSX positions are value positions and if is a statement. But it can appear in the component body, before the return, where it decides what the return will be. Used as a guard clause, it flattens nesting and keeps the happy path at the top level. Used as an early return, it handles loading, error, and empty states before the main render.
This chapter covers the if statement in the component body, early returns, guard clauses, the ternary operator as an expression, the logical && operator, the switch statement, and the patterns that keep conditional rendering readable.
Key point: The if statement is a statement and cannot appear inside JSX. Use it in the component body before the return for early returns, guard clauses, and multi-branch decisions. For conditions inside JSX, use the ternary operator or the logical && operator, which are expressions. Choose the construct that matches the shape of the decision: if for early returns, ternary for either-or values, && for optional content, and switch for many cases.
Why conditional rendering matters
The state-driven UI problem. A component’s output depends on its state: loading, error, empty, or content. The component must decide which of these to render, and the decision is expressed with a conditional. Without conditional rendering, the component would render the same markup regardless of state, which is not a UI.
The statement-versus-expression problem. JSX positions are value positions. A statement like if does not produce a value, so it cannot appear inside JSX. An expression like a ? b : c produces a value, so it can. The distinction determines where each construct can be used.
The readability problem. A component with deeply nested conditionals is hard to read. Early returns and guard clauses flatten the nesting by handling the special cases first and leaving the main render at the top level.
The correctness problem. The logical && operator has a subtle behavior: when the left side is 0, it renders 0 rather than nothing. This is a common bug in conditional rendering, and understanding it prevents it.
The maintenance problem. When the number of branches grows, the ternary becomes unreadable and the switch becomes the right tool. Choosing the construct that matches the number of branches keeps the code maintainable.
a. The if statement in the component body
The if statement appears in the component body, before the return. It decides what the return will be.
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please log in.</h1>;
}
The first if returns the logged-in greeting. If the condition is false, the function continues to the second return, which returns the login prompt. The if is a statement, and the return inside it produces a value that the component returns.
The pattern is called an early return. The special case is handled first, and the main case is the final return. Multiple early returns handle multiple special cases.
function UserProfile({ user, isLoading, error }) {
if (isLoading) {
return <Spinner />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!user) {
return <p>No user found.</p>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Each if handles a state: loading, error, or no user. The final return is the main render, and it runs only when none of the special cases apply. The nesting is flat, and each condition is checked once.
b. Guard clauses
A guard clause is an early return that handles a condition that would otherwise require nesting. The condition is inverted, and the function returns when the condition is not met.
function UserProfile({ user }) {
if (!user) {
return <p>No user found.</p>;
}
return <h1>{user.name}</h1>;
}
Without the guard clause, the component would need to check user inside the JSX:
// Without the guard clause
function UserProfile({ user }) {
return (
<div>
{user ? <h1>{user.name}</h1> : <p>No user found.</p>}
</div>
);
}
The guard clause flattens the structure. The special case is handled first, and the main render assumes the condition is met. This is the same technique used in functions across every language, and it works the same way in React components.
A component can have multiple guard clauses, one for each condition that must be met before the main render.
function Article({ article, author, category }) {
if (!article) return <p>Article not found.</p>;
if (!author) return <p>Author not found.</p>;
if (!category) return <p>Category not found.</p>;
return (
<article>
<h1>{article.title}</h1>
<p>By {author.name}</p>
<p>Category: {category.name}</p>
</article>
);
}
Each guard handles one condition. The main render runs only when all three conditions are met, and it does not need to check any of them.
c. The ternary operator
The ternary operator is an expression, so it can appear inside JSX. It is the right tool for an either-or choice.
function Greeting({ isLoggedIn }) {
return <h1>{isLoggedIn ? 'Welcome back!' : 'Please log in.'}</h1>;
}
The ternary produces a value, and the value is rendered. The syntax is condition ? valueIfTrue : valueIfFalse.
The ternary can also select between two elements:
function Status({ isLoading }) {
return (
<div>
{isLoading ? <Spinner /> : <Content />}
</div>
);
}
Both branches must produce a value that JSX can render. An element, a string, a number, null, and undefined are all valid.
The ternary can be nested, but nesting more than two levels hurts readability.
// Hard to read
{isLoading ? <Spinner /> : error ? <Error /> : <Content />}
// Better
function Status({ isLoading, error }) {
if (isLoading) return <Spinner />;
if (error) return <Error message={error} />;
return <Content />;
}
When the branches are simple and there are only two, the ternary is concise. When the branches are complex or there are more than two, the if statement or the switch is clearer.
d. The logical AND operator
The && operator renders the right side when the left side is truthy, and renders nothing when the left side is falsy.
function Notification({ message }) {
return (
<div>
{message && <p className="notification">{message}</p>}
</div>
);
}
When message is a non-empty string, the expression produces the <p> element, and it is rendered. When message is null, undefined, or an empty string, the expression produces the falsy value, and nothing is rendered.
The && operator is the shorthand for “render this if the condition is true, otherwise render nothing.” It is common for optional content: a badge, an error message, a user avatar.
The subtlety is that the left side is returned when it is falsy, not just skipped. For most falsy values — null, undefined, false, '' — React renders nothing. But for 0, React renders the number 0.
function ItemCount({ count }) {
return (
<div>
{count && <span>{count} items</span>}
{/* When count is 0, this renders "0" */}
</div>
);
}
The fix is to use an explicit comparison or a ternary:
// Explicit comparison
{count > 0 && <span>{count} items</span>}
// Ternary with null
{count ? <span>{count} items</span> : null}
Both forms render nothing when count is 0. The explicit comparison is the more common fix because it reads as the condition the developer intended.
e. The switch statement
The switch statement handles multiple cases. It is a statement, so it appears in the component body, not inside JSX.
function StatusMessage({ status }) {
switch (status) {
case 'loading':
return <Spinner />;
case 'error':
return <ErrorMessage />;
case 'success':
return <SuccessMessage />;
default:
return null;
}
}
Each case returns a different element. The default case handles the values that are not covered. This is the pattern for a component that renders differently based on an enum-like value.
The switch statement does not fall through when each case returns. In JavaScript, switch cases fall through unless a break or return ends them. In a component, the return ends the case, so no break is needed.
A switch can also be used with a discriminated union:
function Shape({ shape }) {
switch (shape.type) {
case 'circle':
return <Circle radius={shape.radius} />;
case 'square':
return <Square side={shape.side} />;
case 'triangle':
return <Triangle base={shape.base} height={shape.height} />;
default:
return <p>Unknown shape</p>;
}
}
The shape.type determines which case runs, and each case uses the fields that are specific to that shape. TypeScript can narrow the type inside each case if the union is discriminated.
f. Choosing the right construct
The choice among if, ternary, &&, and switch depends on the shape of the decision.
| Decision | Construct |
|---|---|
| Early return for a special case | if |
| Multiple early returns | if chain |
| Either-or value inside JSX | Ternary |
| Optional content | && with a truthy check |
| Optional content with a number | Explicit comparison or ternary |
| Many cases | switch |
| Nested conditions | Guard clauses and early returns |
The if statement is the most general. It can handle any number of branches, and it can use any condition. It is the right tool when the decision is about whether to render at all, or when the number of branches is large.
The ternary is the most concise for either-or choices inside JSX. It produces a value, so it fits in any expression position.
The && operator is the shorthand for optional content. It is the right tool when the condition is a simple truthy check and the falsy case renders nothing.
The switch is the right tool when the decision is based on a value with several possibilities. It makes the cases explicit and the default case visible.
Complete Example Session
// ============================================
// PART 1: EARLY RETURN
// ============================================
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please log in.</h1>;
}
// ============================================
// PART 2: MULTIPLE EARLY RETURNS
// ============================================
function UserProfile({ user, isLoading, error }) {
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (!user) return <p>No user found.</p>;
return <h1>{user.name}</h1>;
}
// ============================================
// PART 3: GUARD CLAUSES
// ============================================
function Article({ article, author }) {
if (!article) return <p>Article not found.</p>;
if (!author) return <p>Author not found.</p>;
return (
<article>
<h1>{article.title}</h1>
<p>By {author.name}</p>
</article>
);
}
// ============================================
// PART 4: TERNARY
// ============================================
function Greeting({ isLoggedIn }) {
return <h1>{isLoggedIn ? 'Welcome back!' : 'Please log in.'}</h1>;
}
// ============================================
// PART 5: TERNARY WITH ELEMENTS
// ============================================
function Status({ isLoading }) {
return (
<div>
{isLoading ? <Spinner /> : <Content />}
</div>
);
}
// ============================================
// PART 6: LOGICAL AND
// ============================================
function Notification({ message }) {
return (
<div>
{message && <p className="notification">{message}</p>}
</div>
);
}
// ============================================
// PART 7: THE ZERO PITFALL
// ============================================
function ItemCount({ count }) {
return (
<div>
{count > 0 && <span>{count} items</span>}
</div>
);
}
// ============================================
// PART 8: SWITCH STATEMENT
// ============================================
function StatusMessage({ status }) {
switch (status) {
case 'loading':
return <Spinner />;
case 'error':
return <ErrorMessage />;
case 'success':
return <SuccessMessage />;
default:
return null;
}
}
// ============================================
// PART 9: DISCRIMINATED UNION
// ============================================
function Shape({ shape }) {
switch (shape.type) {
case 'circle':
return <Circle radius={shape.radius} />;
case 'square':
return <Square side={shape.side} />;
default:
return <p>Unknown shape</p>;
}
}
// ============================================
// PART 10: COMBINING CONSTRUCTS
// ============================================
function UserList({ users, isLoading, error }) {
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error} />;
if (users.length === 0) return <p>No users found.</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
{user.isAdmin && <span className="badge">Admin</span>}
</li>
))}
</ul>
);
}
These ten parts cover early return, multiple early returns, guard clauses, the ternary, the ternary with elements, the logical && operator, the zero pitfall, the switch statement, a discriminated union, and combining constructs.
Quick Reference
Conditional Constructs
| Construct | Position | Use case |
|---|---|---|
if | Component body | Early return, guard clause |
| Ternary | Anywhere | Either-or value |
&& | Inside JSX | Optional content |
switch | Component body | Multiple cases |
The Ternary
| Form | Example |
|---|---|
| Value | {x ? 'yes' : 'no'} |
| Elements | {x ? <A /> : <B />} |
| With null | {x ? <A /> : null} |
| Nested | {a ? <A /> : b ? <B /> : <C />} |
The && Operator
| Condition | Renders |
|---|---|
| Truthy | The right side |
null / undefined | Nothing |
false | Nothing |
'' | Nothing |
0 | 0 (pitfall) |
The Zero Pitfall
| Wrong | Right |
|---|---|
{count && <Badge />} | {count > 0 && <Badge />} |
| — | {count ? <Badge /> : null} |
Choosing a Construct
| Decision | Construct |
|---|---|
| Early return | if |
| Multiple special cases | if chain |
| Either-or value | Ternary |
| Optional content | && |
| Many cases | switch |
Best Practices
✅ Do This:
// Early return for special cases
if (isLoading) return <Spinner />;
if (error) return <Error />;
// Ternary for either-or
{isLoggedIn ? <Dashboard /> : <Login />}
// && for optional content
{hasError && <ErrorMessage />}
// Explicit comparison to avoid the zero pitfall
{count > 0 && <Badge count={count} />}
// Switch for many cases
switch (status) {
case 'loading': return <Spinner />;
case 'error': return <Error />;
default: return null;
}
❌ Don’t Do This:
// if inside JSX
return <div>{if (x) { <A /> }}</div>; // ❌
// && with a number
{count && <Badge />} // ❌ renders "0" when count is 0
// Deeply nested ternaries
{a ? <A /> : b ? <B /> : c ? <C /> : <D />} // ❌
// Rendering false or null as text
{x ? 'yes' : false} // ⚠️ renders nothing, but be careful
// Switch with fallthrough
switch (x) { case 'a': return <A />; case 'b': /* falls through */ }
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
0 rendered unexpectedly | && with a number | Use count > 0 && |
if inside JSX | if is a statement | Move it to the component body |
| Nested ternaries unreadable | Too many branches | Use if or switch |
Missing default case | Forgot the fallback | Add default |
| Switch fallthrough | Missing break or return | Add return in each case |
| Empty string rendered | '' is falsy but renders nothing | Verify the condition |
Real-World Examples
1. Loading State
if (isLoading) return <Spinner />;
2. Error State
if (error) return <ErrorMessage message={error} />;
3. Empty State
if (items.length === 0) return <p>No items</p>;
4. Ternary Login
{isLoggedIn ? <Dashboard /> : <Login />}
5. Optional Badge
{isAdmin && <Badge>Admin</Badge>}
6. Zero Safe
{count > 0 && <span>{count}</span>}
7. Switch on Status
switch (status) {
case 'loading': return <Spinner />;
case 'error': return <Error />;
default: return <Content />;
}
8. Discriminated Union
switch (shape.type) {
case 'circle': return <Circle {...shape} />;
case 'square': return <Square {...shape} />;
}
9. Combined
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <List items={items} />;
10. Guard Clause
if (!user) return <p>No user</p>;
return <h1>{user.name}</h1>;
Visual
Statement vs Expression
┌──────────────────────────────────────────────────────────────┐
│ STATEMENT (component body): │
│ if (isLoading) { return <Spinner />; } │
│ └── Cannot appear inside JSX │
│ │
│ EXPRESSION (inside JSX): │
│ {isLoading ? <Spinner /> : <Content />} │
│ └── Produces a value, valid in JSX │
└──────────────────────────────────────────────────────────────┘
Early Return Flow
┌──────────────────────────────────────────────────────────────┐
│ function Component({ data, isLoading, error }) { │
│ if (isLoading) return <Spinner />; │
│ if (error) return <Error />; │
│ if (!data) return <Empty />; │
│ return <Content data={data} />; │
│ } │
│ │
│ Each condition is checked once. │
│ The main render is at the top level. │
└──────────────────────────────────────────────────────────────┘
The Zero Pitfall
┌──────────────────────────────────────────────────────────────┐
│ count = 0: │
│ │
│ {count && <Badge />} │
│ └── 0 && <Badge /> → 0 │
│ └── React renders the number 0 │
│ │
│ {count > 0 && <Badge />} │
│ └── 0 > 0 → false │
│ └── React renders nothing │
└──────────────────────────────────────────────────────────────┘
Choosing a Construct
┌──────────────────────────────────────────────────────────────┐
│ How many branches? │
│ │ │
│ ├── 0 or 1 (optional content) ──▶ && │
│ │ │
│ ├── 2 (either-or) ──▶ ternary │
│ │ │
│ └── 3+ ──▶ switch or early returns │
│ │
│ Where is the decision? │
│ │ │
│ ├── Component body ──▶ if or switch │
│ │ │
│ └── Inside JSX ──▶ ternary or && │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
if statement | Component body, early return, guard clause |
| Ternary | Expression, either-or value |
&& | Expression, optional content |
switch | Component body, multiple cases |
if in JSX | Not allowed; it is a statement |
| Zero pitfall | count && <Badge /> renders 0 |
| Fix for zero | count > 0 && <Badge /> |
| Guard clause | Inverted condition, early return |
| Nested ternary | Avoid beyond two levels |
| Switch fallthrough | Prevented by return in each case |
Key takeaways:
- The
ifstatement is a statement and cannot appear inside JSX. Use it in the component body, before thereturn, for early returns and guard clauses. - Early returns handle special cases first. Loading, error, and empty states are handled before the main render, and the main render assumes the conditions are met. The nesting is flat, and each condition is checked once.
- The ternary is an expression and can appear inside JSX. It is the right tool for either-or choices. Both branches must produce a value that JSX can render.
- The
&&operator is the shorthand for optional content. It renders the right side when the left side is truthy, and nothing when it is falsy. The exception is0, which is falsy but renders as the number0. - The zero pitfall has a simple fix. Use an explicit comparison (
count > 0 &&) or a ternary withnull(count ? <Badge /> : null). Both render nothing when the value is0. - The
switchstatement handles multiple cases. It appears in the component body, and each case returns a different element. Thedefaultcase handles the values that are not covered. - Choose the construct that matches the decision.
iffor early returns, ternary for either-or values,&&for optional content, andswitchfor many cases. The construct should make the decision readable, not obscure it.
Remember: Conditional rendering is how a React component decides what to show based on its state. The if statement is the most general tool, and it belongs in the component body where it can return early and flatten the nesting. The ternary and && are expressions, and they belong inside JSX where a value is expected. The switch handles many cases. The zero pitfall is the one subtlety that catches everyone at least once: 0 && <Badge /> renders 0, not nothing. The fix is an explicit comparison. Once these constructs and their positions are understood, conditional rendering becomes a matter of choosing the one that reads most clearly for the decision at hand.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!