Conditional Rendering
Show, hide, or swap UI based on state, props, or computed values.
Search across all documentation pages
Show, hide, or swap UI based on state, props, or computed values.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
Status) driving four distinct UI states&& operator to render each state branch independentlyresult.data is safely accessed only in the "success" branchfalse, null, undefined, and true are valid JSX children that render nothing, which is why && and ternaries work<Spinner /> to <DataTable />), React unmounts the old tree and mounts a fresh one| 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] |
const statusUI: Record<Status, React.ReactNode> = {
idle: <p>Waiting...</p>,
loading: <Spinner />,
success: <DataTable />,
error: <ErrorBanner />,
};
return <div>{statusUI[status]}</div>;// 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} />;
}
}0 && renders a literal 0 - {count && <Badge />} outputs 0 on screen when count is zero because 0 is a falsy but renderable value. Fix: Use {count > 0 && <Badge />} or a ternary.
Missing null return - A component that conditionally returns JSX but has no fallback return implicitly returns undefined, which works but triggers linting warnings. Fix: Explicitly return null for 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.
| 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 |
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:
return null statements act as guard clauses, preventing rendering when nothing useful can be shownloading && locationBanners.length === 0 means show nothing while loading the first time, but keep showing stale banners during a refreshbanners[location] || [] provides a safe fallback so .length never throws on undefinedcn() utility (clsx + tailwind-merge) conditionally joins class namesUse 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.
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.
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>;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.
display: noneA 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.
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.
display: none when you want to keep the component mounted and preserve its state (e.g., tab panels)Return null. This is cleaner than returning an empty Fragment <></> and is the standard pattern for components that conditionally have no output.
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>;Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥