Skip to content
React rc rendering 4 min read

Conditional Rendering Patterns

Basic ternaries and && are enough for one or two branches, but real screens have many states: loading, error, empty, success, and a fistful of status enums. Cramming all of that into nested ternaries produces JSX that nobody can read. The patterns on this page—guard returns, lookup maps, and component extraction—keep conditional UI flat, declarative, and easy to extend as states multiply.

Guard returns for sequential states

When a component must pass through several mutually exclusive states before reaching its “happy path,” handle each one with an early return at the top. Each guard answers one question and exits, so by the time you reach the final return you know every precondition is satisfied. This reads top-to-bottom like a checklist and never nests.

function UserCard({ status, error, user }) {
  if (status === "loading") return <Spinner />;
  if (status === "error") return <ErrorBanner message={error} />;
  if (!user) return <EmptyState label="No user found" />;

  return (
    <article className="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </article>
  );
}

Compare that to the equivalent nested ternary—status === "loading" ? <Spinner /> : status === "error" ? ... : ...—which forces the reader to balance parentheses in their head. Guard returns scale linearly: adding a fourth state is one more line, not another layer of nesting.

Guards must come before any conditional return null, but after all your hooks. Hooks must run in the same order on every render, so never put a return above a useState or useEffect call.

Mapping a value to a component with a lookup object

When a single variable selects one of N components, a lookup object is dramatically cleaner than a chain of if/else if or a switch. You define a plain object whose keys are the possible values and whose values are the components (or elements), then index into it. Adding a case is a one-line edit, and the data structure is trivial to test.

const TAB_VIEWS = {
  overview: OverviewPanel,
  activity: ActivityPanel,
  settings: SettingsPanel,
};

function Tabs({ active }) {
  const Panel = TAB_VIEWS[active] ?? NotFoundPanel;
  return <Panel />;
}

Because Panel is a capitalized variable, JSX treats it as a component and renders it. The ?? NotFoundPanel fallback handles any value missing from the map, so an unexpected key never crashes the UI. If each branch needs different props, store a render function instead of a bare component:

const STATUS_BADGE = {
  active: () => <span className="badge badge--green">Active</span>,
  paused: () => <span className="badge badge--gray">Paused</span>,
  banned: (name) => <span className="badge badge--red">{name} banned</span>,
};

function Badge({ status, name }) {
  const render = STATUS_BADGE[status] ?? (() => null);
  return render(name);
}

Rendering enums and status values

Status enums—order states, request lifecycles, permission levels—map perfectly onto the lookup pattern, but you often want both an icon and a label per value. Keep the presentation data in a config object and let the component stay dumb. This separates what each status looks like (data) from how it’s drawn (one piece of JSX).

const ORDER_STATES = {
  pending:   { label: "Pending",   icon: "clock",  tone: "warn" },
  shipped:   { label: "Shipped",   icon: "truck",  tone: "info" },
  delivered: { label: "Delivered", icon: "check",  tone: "ok"   },
  cancelled: { label: "Cancelled", icon: "x",      tone: "error" },
};

function OrderStatus({ state }) {
  const cfg = ORDER_STATES[state];
  if (!cfg) return null;

  return (
    <span className={`status status--${cfg.tone}`}>
      <Icon name={cfg.icon} /> {cfg.label}
    </span>
  );
}

Output (when state is "shipped"):

🚚 Shipped

To add a refunded state, you add one entry to ORDER_STATES—no JSX changes. This is the single biggest readability win over a switch that interleaves data and markup.

Avoiding deeply nested ternaries

A two-way ternary is fine; a three-way one is a smell. When you find yourself writing a ? b : c ? d : e, refactor using one of the table’s strategies below. The goal is that no return contains more than one ?.

SituationAnti-patternBetter pattern
Several exclusive statesNested ternaryGuard returns
Value selects a componentswitch in JSXLookup object
Status with icon + labelInline if/elseConfig object
Big branch grows in markupInline JSXExtracted component

The last row matters most. When a branch’s JSX exceeds a few lines, pull it into its own component. The parent then becomes a clean dispatcher, and each branch gains its own name, props, and testability.

function Notification({ kind, payload }) {
  if (kind === "mention") return <MentionNotice {...payload} />;
  if (kind === "reply")   return <ReplyNotice {...payload} />;
  return <GenericNotice {...payload} />;
}

Best Practices

  • Use guard returns for sequential, mutually exclusive states—loading, error, empty—before the happy path.
  • Keep all hook calls above your conditional returns so hook order stays stable.
  • Replace long if/else if chains with a lookup object keyed by the deciding value.
  • Always provide a fallback (?? Fallback) when indexing a lookup map with dynamic data.
  • Store status presentation as a config object so adding a state is a data change, not a JSX change.
  • Never nest ternaries beyond one level; refactor to guards, maps, or extracted components.
  • Extract any branch whose JSX grows past a few lines into a named child component.
Last updated June 14, 2026
Was this helpful?