React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

conditionalternarypatternsrendering

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.data is 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, and true are 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

PatternBest ForExample
Ternary ? :Choosing between two elements{ok ? <A /> : <B />}
&&Showing or hiding one element{show && <Modal />}
Early returnGuarding the entire componentif (!data) return null;
Variable extractionComplex multi-branch logicconst ui = ...;
Map/lookup objectMany discrete statesstatusMap[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 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.

Alternatives

AlternativeUse WhenDon't Use When
CSS display: noneYou 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> + lazyConditionally loading an entire code-split chunkSimple show/hide within an already-loaded component
Route-based renderingDifferent "pages" that each have their own URLToggle 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 null statements act as guard clauses, preventing rendering when nothing useful can be shown
  • loading && locationBanners.length === 0 means show nothing while loading the first time, but keep showing stale banners during a refresh
  • banners[location] || [] provides a safe fallback so .length never 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: none when 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>;
  • JSX and TSX — expression slots where conditionals live
  • Lists and Keys — rendering dynamic collections
  • Components — structuring conditional branches as separate components