Conditional Rendering
Conditional rendering is how a React component decides what to display based on data — showing a loading spinner while data fetches, a login form versus a welcome message, or an error banner only when something goes wrong. Because JSX is just JavaScript, you don’t need a special templating syntax for this: you reach for the same tools you already know, like if statements, ternary expressions, and logical operators, and use them to decide what JSX gets returned or embedded.
This lesson covers every common pattern for conditional rendering in React, explains how each one behaves under the hood, and walks through the mistakes that trip up almost every React developer at least once.
Overview / How it works
A React function component is just a function that returns JSX (or null). Whatever JSX expression the function returns during a render is what React reconciles against the previous render and commits to the DOM. Conditional rendering means the returned JSX — or a piece of it — differs depending on props, state, or other computed values.
There is no dedicated “if” tag in JSX. Instead, JSX curly braces {} let you drop any JavaScript expression into the markup, and expressions can include ternaries (condition ? a : b) and logical AND (condition && a). Full if/else statements cannot be embedded directly inside a JSX tree (because JSX compiles to function calls, and statements aren’t expressions), but you can absolutely use if/else before the return statement to decide which JSX to return, or to compute a variable that you then embed.
Two special rendering values matter here: returning null (or false, or undefined) from a component, or embedding one of those values in JSX, tells React to render nothing at that spot — no DOM node is created. This is different from rendering an empty string or a hidden element; the element genuinely does not exist in the tree. When the condition later changes and the component starts returning real JSX, React mounts a brand new subtree (running effects, refs, etc. for the first time), rather than “unhiding” something that was already there.
Because this all happens during the render phase, conditional rendering is fully declarative: you don’t imperatively show/hide elements (like calling .style.display in vanilla JS). You simply describe, for a given state, what the UI should look like — and when state changes, React re-runs the component function and figures out the minimal DOM changes needed via reconciliation.
Syntax
There are four common patterns for conditional rendering in React:
// 1. if / else before return
function Component({ condition }) {
if (condition) {
return <p>True branch</p>;
}
return <p>False branch</p>;
}
// 2. Ternary operator inside JSX
<p>{condition ? "True branch" : "False branch"}</p>
// 3. Logical AND (&&) for show/hide
<p>{condition && "Only shown when truthy"}</p>
// 4. Returning null to render nothing
if (!condition) return null;
| Pattern | Best for | Notes |
|---|---|---|
if / else |
Choosing between two (or more) full return values | Must live outside the JSX, before return |
Ternary ? : |
Inline either/or inside JSX | Keep it short; avoid nesting ternaries |
&& |
Show something or nothing (no “else”) | Left side must not be a number that could be 0 |
Early return null |
Skip rendering the whole component | Common for guard clauses (e.g. no data yet) |
switch / object map |
Many mutually exclusive states | Compute JSX into a variable, then return it |
Examples
Example 1: Ternary for a two-way branch
function LoginStatus({ isLoggedIn }) {
return (
<p>
{isLoggedIn ? "Welcome back!" : "Please log in."}
</p>
);
}
What it renders: If isLoggedIn is true, a paragraph reading “Welcome back!”; otherwise a paragraph reading “Please log in.” The ternary evaluates directly inside the curly braces, so the component always returns exactly one JSX tree — only the text content changes.
Example 2: Logical AND for optional content
function Inbox({ unreadCount }) {
return (
<div>
<h3>Inbox</h3>
{unreadCount > 0 && <span>You have {unreadCount} unread messages</span>}
</div>
);
}
What it renders: The heading “Inbox” always appears. The <span> only renders when unreadCount > 0; when the count is 0, the expression evaluates to false, and React renders nothing for that spot. Note the explicit unreadCount > 0 comparison rather than just unreadCount && — see Common Mistakes below for why that distinction matters.
Example 3: Multiple states with early returns
import { useEffect, useState } from "react";
function UserProfile({ userId }) {
const [status, setStatus] = useState("loading");
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
setStatus("loading");
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (!cancelled) {
setUser(data);
setStatus("success");
}
})
.catch(() => {
if (!cancelled) setStatus("error");
});
return () => {
cancelled = true;
};
}, [userId]);
if (status === "loading") {
return <p>Loading user...</p>;
}
if (status === "error") {
return <p>Something went wrong. Please try again.</p>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
What it renders: On mount, “Loading user…” appears while the fetch is in flight. Once it resolves, the component re-renders and either shows the error message or the user’s name and email. This pattern — guard clauses with early return statements for each mutually exclusive state — keeps the “happy path” JSX at the bottom uncluttered by conditional noise, and scales cleanly to more states (e.g. adding an “empty” status) without nesting.
How it works step by step
On initial mount: React calls the component function once. Whatever expression the returned JSX evaluates to — including any ternaries or && checks — determines the very first DOM nodes that get created. If a branch renders null, no node is created for it at all.
On a state or prop update: When setState is called (or a parent re-renders with new props), React re-invokes the component function from scratch. The conditional expressions are re-evaluated with the current values. React then diffs the newly returned JSX tree against the previous one (reconciliation) and applies only the minimal DOM mutations needed — inserting new nodes, removing ones that are no longer rendered, or updating text/attributes on nodes that persist.
Mounting vs. unmounting a branch: If a conditional branch goes from rendering an element to rendering null (or vice versa), that subtree is fully unmounted or fully mounted — not hidden and shown. This matters for hooks: any useEffect in that subtree runs its cleanup function on unmount and its setup function again on the next mount, and any local useState in that subtree resets to its initial value each time it remounts.
Common Mistakes
Mistake 1: Using && with a number that can be zero.
// Wrong: renders the literal text "0" when the cart is empty
function CartBadge({ itemCount }) {
return <div>{itemCount && <span>{itemCount} items</span>}</div>;
}
When itemCount is 0, JavaScript’s && returns the left-hand value itself (0), not false. React does render the number 0 (unlike false/null/undefined, which render nothing), so the page shows a stray “0” instead of nothing. Fix it by making the condition explicitly boolean:
// Correct: comparison produces a real boolean
function CartBadge({ itemCount }) {
return <div>{itemCount > 0 && <span>{itemCount} items</span>}</div>;
}
Mistake 2: Trying to put an if statement directly inside JSX.
// Wrong: `if` is a statement, not an expression — this is a syntax error
function Greeting({ isMorning }) {
return (
<div>
{if (isMorning) { "Good morning" } else { "Good evening" }}
</div>
);
}
Curly braces in JSX only accept expressions, and if/else are statements — this will fail to compile. Use a ternary inside the JSX, or move the if/else above the return and assign the result to a variable:
// Correct: ternary expression
function Greeting({ isMorning }) {
return <div>{isMorning ? "Good morning" : "Good evening"}</div>;
}
Best Practices
- Use a ternary when there are exactly two branches and both are short; use
&&only when there is genuinely no “else” case. - With
&&, always make sure the left-hand side is a real boolean (use a comparison likecount > 0, not a raw number or string). - Avoid nesting ternaries inside ternaries — extract the logic into an early return, a named variable, or a small helper component instead.
- For three or more mutually exclusive states (loading/error/success/empty), prefer early
returnstatements or a lookup object/switchover chained ternaries. - Return
nullexplicitly when a component should render nothing for a given state — it’s clearer than returning an empty string or a hidden element. - Remember that toggling between rendering an element and rendering
nullunmounts/remounts that subtree, resetting its local state and re-running its effects — use CSS-based hiding instead if you need to preserve state while visually hiding something.
Practice Exercises
1. Build a ToggleMessage component with a useState boolean and a button. Clicking the button should toggle between showing the text “Message is visible” and rendering nothing at all.
2. Build a StatusBadge component that accepts a status prop of "success", "error", or "pending", and renders a different message for each using either a switch statement or a lookup object — no chained ternaries allowed.
3. The following component has a bug: when score is 0, the page shows a stray “0” instead of nothing. Rewrite it so a score of 0 renders nothing, using the pattern shown in Common Mistakes.
function ScoreDisplay({ score }) {
return <div>{score && <p>Score: {score}</p>}</div>;
}
Summary
- JSX has no built-in “if” tag — conditional rendering uses ordinary JavaScript expressions inside
{}, orif/elsebefore thereturn. - Ternaries (
condition ? a : b) suit two-way branches;&&suits “render this or nothing”; early returns and lookup objects suit many states. null,false, andundefinedrender nothing, but the number0renders literally — always coerce&&conditions to real booleans.- Switching a branch between an element and
nullmounts/unmounts that subtree, resetting its state and re-running its effects. - Prefer readable structure (early returns, extracted variables) over deeply nested ternaries as the number of states grows.
