Conditional Rendering
Show, hide, or swap UI based on state, props, or computed values.
Recipe
Quick-reference recipe card — copy-paste ready.
// Ternary — pick between two elements
{isLoggedIn ? <Dashboard /> : <LoginForm />}
// Logical AND — show or nothing
{hasNotifications && <Badge count={notifications.length} />}
// Early return — bail out of the whole component
if (!user) return <Skeleton />;
return <Profile user={user} />;
// Extracted variable — readable when logic is complex
const content = status === "loading"
? <Spinner />
: status === "error"
? <ErrorMessage />
: <DataTable rows={data} />;
return <section>{content}</section>;When to reach for this: Any time the UI changes based on a condition — loading states, auth gates, feature flags, empty states.
Working Example
"use client";
import { useState } from "react";
type Status = "idle" | "loading" | "success" | "error";
interface FetchResult {
status: Status;
data?: string[];
error?: string;
}
export function FetchDemo() {
const [result, setResult] = useState<FetchResult>({ status: "idle" });
async function handleFetch() {
setResult({ status: "loading" });
try {
// Simulate network request
await new Promise(resolve => setTimeout(resolve, 1000));
const success = Math.random() > 0.3;
if (!success) throw new Error("Network timeout");
setResult({
status: "success",
data: ["React", "Next.js", "TypeScript"],
});
} catch (err) {
setResult({
status: "error",
error: err instanceof Error ? err.message : "Unknown error",
});
}
}
return (
<div className="max-w-sm space-y-4 rounded border p-4">
<button
onClick={handleFetch}
disabled={result.status === "loading"}
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
{result.status === "loading" ? "Fetching..." : "Fetch Data"}
</button>
{result.status === "idle" && (
<p className="text-gray-500">Press the button to load data.</p>
)}
{result.status === "loading" && (
<div className="animate-pulse rounded bg-gray-200 p-4">Loading...</div>
)}
{result.status === "error" && (
<div className="rounded bg-red-50 p-3 text-red-700">
<strong>Error:</strong> {result.error}
</div>
)}
{result.status === "success" && result.data && (
<ul className="list-inside list-disc">
{result.data.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}What this demonstrates:
- Discriminated union (
Status) driving four distinct UI states &&operator to render each state branch independently- Disabled button with conditional label text
- Type narrowing —
result.datais safely accessed only in the"success"branch
Deep Dive
How It Works
- React renders whatever the component function returns — there's no special template syntax for conditionals
false,null,undefined, andtrueare valid JSX children that render nothing, which is why&&and ternaries work- When a conditional flips and the component type at a position changes (e.g.,
<Spinner />to<DataTable />), React unmounts the old tree and mounts a fresh one - When the component type stays the same but props change, React updates in place
Patterns Compared
| Pattern | Best For | Example |
|---|---|---|
Ternary ? : | Choosing between two elements | {ok ? <A /> : <B />} |
&& | Showing or hiding one element | {show && <Modal />} |
| Early return | Guarding the entire component | if (!data) return null; |
| Variable extraction | Complex multi-branch logic | const ui = ...; |
| Map/lookup object | Many discrete states | statusMap[status] |
Lookup Object Pattern
const statusUI: Record<Status, React.ReactNode> = {
idle: <p>Waiting...</p>,
loading: <Spinner />,
success: <DataTable />,
error: <ErrorBanner />,
};
return <div>{statusUI[status]}</div>;TypeScript Notes
// Discriminated unions make conditional rendering type-safe
type Result =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };
function Display({ result }: { result: Result }) {
switch (result.status) {
case "idle":
return <p>Ready</p>;
case "loading":
return <Spinner />;
case "success":
// result.data is narrowed to string[] here
return <List items={result.data} />;
case "error":
// result.error is narrowed to string here
return <Alert message={result.error} />;
}
}Gotchas
-
0 &&renders a literal0—{count && <Badge />}outputs0on screen when count is zero because0is a falsy but renderable value. Fix: Use{count > 0 && <Badge />}or a ternary. -
Missing
nullreturn — A component that conditionally returns JSX but has no fallback return implicitly returnsundefined, which works but triggers linting warnings. Fix: Explicitlyreturn nullfor the empty case. -
State reset on type change — Switching between
<input type="text" />and<textarea />at the same tree position destroys state because they're different element types. Fix: If you need to preserve state, render both and toggle visibility with CSS, or lift state up. -
Nested ternaries —
{a ? b ? <X /> : <Y /> : <Z />}is hard to read and easy to misparse. Fix: Extract into a variable or use a switch/map pattern.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
CSS display: none | You want to keep the component mounted and preserve its state (e.g., tab panels) | The hidden content is expensive to render or involves network requests |
<Suspense> + lazy | Conditionally loading an entire code-split chunk | Simple show/hide within an already-loaded component |
| Route-based rendering | Different "pages" that each have their own URL | Toggle within a single view |
Real-World Example
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: Banner display with multiple guard clauses
// File: src/components/banner/banner-display.tsx
export default function BannerDisplay({ location, className }: BannerDisplayProps) {
const { banners, loading, error, fetchBanners } = useBannerStore();
const locationBanners = banners[location] || [];
useEffect(() => {
fetchBanners(location);
}, [location]);
// Guard clause 1: loading with no cached data
if (loading && locationBanners.length === 0) {
return null;
}
// Guard clause 2: error state
if (error) {
return null;
}
// Guard clause 3: no banners for this location
if (locationBanners.length === 0) {
return null;
}
// Happy path: render banners
return (
<div className={cn('space-y-4', className)}>
{locationBanners.map((banner) => (
<div
key={banner.id}
className={cn(
'rounded-md py-2 px-4 shadow-sm',
getBorderClass(banner.borderStyle),
'bg-zinc-50 dark:bg-zinc-900',
banner.customStyles
)}
>
{banner.content}
</div>
))}
</div>
);
}What this demonstrates in production:
- Three early
return nullstatements act as guard clauses, preventing rendering when nothing useful can be shown loading && locationBanners.length === 0means show nothing while loading the first time, but keep showing stale banners during a refreshbanners[location] || []provides a safe fallback so.lengthnever throws on undefined- The
cn()utility (clsx + tailwind-merge) conditionally joins class names - Design decision: returning null instead of a skeleton prevents layout shift for optional UI like banners
FAQs
What is the best way to conditionally render a component?
Use a ternary {condition ? <A /> : <B />} to choose between two elements, or {condition && <A />} to show or hide one element. For complex multi-branch logic, use a variable or lookup object before the return.
Why does 0 && Component render "0" on screen?
JavaScript's && operator returns the first falsy value. 0 is falsy but is a valid React child that renders as text. Use {count > 0 && <Component />} or a ternary instead.
When should I use early return vs ternary?
- Early return for guarding the entire component (loading, error, not-found states)
- Ternary for inline switches within the JSX tree
- Early returns are more readable when there are multiple guard conditions
What is the lookup object pattern for conditional rendering?
Map state values to JSX elements using a Record:
const statusUI: Record<Status, React.ReactNode> = {
idle: <p>Waiting...</p>,
loading: <Spinner />,
success: <DataTable />,
error: <ErrorBanner />,
};
return <div>{statusUI[status]}</div>;Does conditional rendering unmount and remount components?
Yes, if the component type at a tree position changes (e.g., <Spinner /> to <DataTable />), React unmounts the old component and mounts a new one, destroying all state. If only props change on the same component type, React updates in place.
How do I preserve state when toggling between two views?
- Render both and toggle visibility with CSS
display: none - Lift the state up to a parent component that persists across both views
- Use a shared state store like Zustand
What is a discriminated union and how does it help conditional rendering?
A TypeScript union where each member has a literal status field. The switch or if on status narrows the type, so TypeScript knows which fields are available in each branch — eliminating undefined checks.
Is it bad to use nested ternaries?
Yes, they're hard to read. For more than two branches, extract the logic into a variable, use a switch statement, or use the lookup object pattern.
When should I use CSS display:none vs conditional rendering?
- Use
display: nonewhen you want to keep the component mounted and preserve its state (e.g., tab panels) - Use conditional rendering when the hidden content is expensive to render, fetches data, or runs effects you want to stop
How do I render nothing from a component?
Return null. This is cleaner than returning an empty Fragment <></> and is the standard pattern for components that conditionally have no output.
Can I use switch statements in JSX?
Not directly inside JSX. Extract it to a variable or a helper function:
function getStatusIcon(status: Status) {
switch (status) {
case "success": return <CheckIcon />;
case "error": return <XIcon />;
default: return null;
}
}
return <div>{getStatusIcon(status)}</div>;Related
- JSX and TSX — expression slots where conditionals live
- Lists and Keys — rendering dynamic collections
- Components — structuring conditional branches as separate components