React

Conditional Rendering Patterns in React

A practical look at the different ways to conditionally render content in React — when to use each pattern and which ones to avoid.

Conditional rendering is one of the first things you write in React, and also one of the first things you get wrong. I did. I started with ternaries everywhere, then read that short-circuit evaluation was cleaner, then discovered both can produce subtle bugs depending on what you're rendering.

There are actually several distinct patterns, each with different trade-offs. Here's what I learned about when to reach for each one.

The Basic Ternary

The most readable option for simple if/else rendering:

function StatusBadge({ isActive }: { isActive: boolean }) {
  return (
    <span>
      {isActive ? "Active" : "Inactive"}
    </span>
  )
}

Ternaries work well when both branches render something. The moment one branch returns null, I usually switch to a different pattern — a ternary returning null is harder to scan than a short-circuit.

For nested conditions, ternaries get messy fast:

// Hard to read
{isLoggedIn ? isAdmin ? <AdminPanel /> : <UserPanel /> : <LoginPrompt />}

At that point, pull the condition out of the JSX entirely.

Short-Circuit Evaluation

The && pattern is everywhere in React codebases:

function Notification({ message }: { message: string | null }) {
  return (
    <div>
      {message && <p>{message}</p>}
    </div>
  )
}

This works fine when message is a string or null. The problem is with numbers. If message were typed as number | null and its value was 0, React would render a literal 0 in the DOM — because 0 && <p>...</p> evaluates to 0, not false, and React renders 0 as text.

The safe version uses an explicit boolean check:

{message !== null && <p>{message}</p>}

// Or convert to boolean
{!!message && <p>{message}</p>}

// Or for numbers specifically
{count > 0 && <span>{count} items</span>}

I got burned by this with a counter component. The count was 0, the badge rendered a 0 on the screen, and it took an embarrassingly long time to figure out why.

Early Returns

When a component has significant conditional branches, early returns keep the main render path clean:

function UserProfile({ user }: { user: User | null }) {
  if (!user) {
    return <p>No user found.</p>
  }

  if (user.isSuspended) {
    return <SuspendedMessage />
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  )
}

This reads like regular JavaScript logic. Each exit condition is explicit and the main render path is what you see last. I find this much easier to follow than deeply nested ternaries when there are more than two states.

Early returns also keep TypeScript happy — after the null check, the type narrows and you don't need optional chaining everywhere in the rest of the component.

Extracting to Variables

For conditions that are complex but don't warrant a new component, assign to a variable before the return:

function Dashboard({ role, features }: Props) {
  const showAdminPanel = role === "admin" && features.includes("admin-panel")
  const showBilling = role !== "guest" && features.includes("billing")

  return (
    <main>
      <Navigation />
      {showAdminPanel && <AdminPanel />}
      {showBilling && <BillingSection />}
    </main>
  )
}

The JSX stays flat and readable. The conditions are named and easy to understand at a glance. This is my default when the condition involves more than one operand.

Conditional Classes

This is a slightly different problem, but it comes up constantly: applying CSS classes conditionally. The cleanest approach is a small utility:

function Button({ disabled, variant }: ButtonProps) {
  const className = [
    "btn",
    variant === "primary" ? "btn-primary" : "btn-secondary",
    disabled ? "btn-disabled" : "",
  ]
    .filter(Boolean)
    .join(" ")

  return <button className={className} disabled={disabled}>Click me</button>
}

Most codebases just install clsx or classnames instead of writing this manually:

import clsx from "clsx"

const className = clsx("btn", {
  "btn-primary": variant === "primary",
  "btn-disabled": disabled,
})

When to Extract a Component

A conditional block that is more than a few lines is usually a signal to extract a component. Component composition is a deeper topic, but extracting conditional branches is one of the most common reasons to split a component. If you're writing this:

{isLoading ? (
  <div className="spinner-container">
    <Spinner size="large" />
    <p>Loading your data...</p>
  </div>
) : (
  <DataTable rows={rows} columns={columns} />
)}

The loading state is probably a <LoadingState /> component. The condition then collapses:

{isLoading ? <LoadingState /> : <DataTable rows={rows} columns={columns} />}

Smaller components are also easier to test and reuse.

Mapping Multiple States

Sometimes you have more than two states: loading, error, empty, and populated. A switch or object map handles this without nesting:

type Status = "loading" | "error" | "empty" | "success"

const views: Record<Status, React.ReactNode> = {
  loading: <Spinner />,
  error: <ErrorMessage />,
  empty: <EmptyState />,
  success: <DataTable rows={rows} />,
}

return <div>{views[status]}</div>

This is explicit about every state and easy to add to. The alternative is a ternary chain or a series of early returns — both work, but the object map scales better when states grow beyond three.

Which Pattern to Use

My rough decision tree:

  • Two clear branches (if/else) with JSX on both sides: ternary
  • Optional content that might not render at all: short-circuit with an explicit boolean
  • Multiple early exit conditions: early returns
  • Complex condition logic: assign to a named variable
  • More than two states: object map

The pattern I reach for least often now is nested ternaries. They look compact but they're genuinely hard to read three months later when you've forgotten the context.

Conditional rendering is mostly just JavaScript logic that happens to live inside JSX. Once I stopped treating it as a special React concept, the patterns got easier to reason about.

← Older
Controlled vs Uncontrolled Forms in React
Newer →
Composition Over Prop Drilling in React

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.