Conditional Rendering Patterns

Learn the idiomatic ways to render different UI depending on state — if statements, ternaries, logical && and ||, early returns — and how to handle loading, error, empty, and permission-based UI states cleanly.

React components are just functions that return JSX, which means deciding what to render is ordinary JavaScript control flow. There is no special 'v-if' or 'ng-if' directive — instead you reach for if statements, ternary expressions, short-circuit logical operators, and early returns, each suited to a different shape of decision.

if, ternary, and logical operators

A plain if statement works well when you want to choose between two (or more) completely different JSX trees before the component's main return. Because JSX is an expression, you can also compute a variable holding the JSX to render inside an if/else block, then return that variable at the end of the function.

The ternary operator (condition ? <A /> : <B />) is the most common inline way to choose between two small pieces of JSX directly inside a return statement or inside another JSX expression, such as choosing button text or swapping an icon.

The && operator is used for 'render this, or render nothing' — condition && <Element /> renders the element only when condition is truthy, and renders nothing (technically false, which React does not display) when it is falsy. The || operator is used for fallback values — value || <DefaultElement /> renders value when it is truthy and a default otherwise, which is handy for default avatars, placeholder text, or fallback content.

Nullish values and falsy pitfalls

A classic bug with && is that React renders literal 0 to the screen, because 0 is a valid, displayable value in JSX even though it's falsy in JavaScript. Writing {count && <span>{count} items</span>} when count is 0 renders a literal '0' on the page instead of nothing. The fix is to make the condition explicitly boolean, such as {count > 0 && ...} or {Boolean(count) && ...}.

Nullish values like null, undefined, and false are simply not rendered by React — they produce no output — which is exactly what makes the && pattern useful for conditional pieces of UI. This is different from empty string '' or the number 0, both of which are falsy but do get rendered as their (empty or zero) text.

The nullish coalescing operator (??) is useful for distinguishing 'no value provided' from 'a falsy value was intentionally provided', for example displayName ?? 'Anonymous' correctly falls back only when displayName is null or undefined, while an empty string '' is left as-is (unlike ||, which would replace an empty string too).

Early returns and guard clauses

For components with several mutually exclusive states, stacking ternaries or nested && expressions quickly becomes hard to read. An early return — returning a small piece of JSX directly from an if block near the top of the component — is often clearer: if the data is loading, return a spinner; if there was an error, return an error message; otherwise fall through to the main render.

This pattern is especially common for the loading / error / empty / success states that almost every data-driven component needs to handle: a loading spinner while a request is in flight, an error message if the request failed, an empty state message if the request succeeded but returned no items, and the actual content once data is present.

Permission-based UI follows the same early-return shape: if the current user's role or permission flag doesn't allow access to a section, return null or an 'access denied' message before reaching the code that would render sensitive controls, rather than trying to conditionally hide individual buttons deep inside a large render tree.

Common conditional rendering forms

Use count > 0 && ... instead of count && ... to avoid React rendering a stray literal 0. Early returns must happen before any other Hook calls that depend on being called unconditionally — keep Hooks above the guard clauses.

// Ternary
{isLoggedIn ? <Dashboard /> : <LoginForm />}

// Logical AND (render or nothing)
{unreadCount > 0 && <Badge count={unreadCount} />}

// Logical OR (fallback content)
{user.avatarUrl || <DefaultAvatar />}

// Early return (guard clause)
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
if (items.length === 0) return <EmptyState />;

Code Examples

A ternary directly inside the JSX picks between two small headings depending on the isLoggedIn boolean prop. Expected result: Renders 'Welcome back!' when isLoggedIn is true, otherwise 'Please sign in.'

Early returns handle the loading, error, and empty states cleanly at the top of the component, so the final return only has to deal with the success case. The canEdit && <button> pattern additionally hides the edit button from users without permission. Ordering matters: loading is checked first (data isn't ready to inspect yet), then error (a failed request should not fall through to 'no items'), then the empty-array case, and only then the real content.

Common mistake: Checking items.length === 0 before checking error would show a misleading 'No items yet' message when the real problem is that the request failed.

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
  return (
    <div>
      {isLoggedIn ? (
        <h1>Welcome back!</h1>
      ) : (
        <h1>Please sign in.</h1>
      )}
    </div>
  );
}
interface Item { id: string; name: string }

function ItemList({
  isLoading,
  error,
  items,
  canEdit,
}: {
  isLoading: boolean;
  error: string | null;
  items: Item[];
  canEdit: boolean;
}) {
  if (isLoading) {
    return <p>Loading items...</p>;
  }

  if (error) {
    return <p role="alert">Error: {error}</p>;
  }

  if (items.length === 0) {
    return <p>No items yet. Add your first one!</p>;
  }

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}
          {canEdit && <button>Edit</button>}
        </li>
      ))}
    </ul>
  );
}

Guard clause state machine

Each guard clause exits early, leaving only the true success case to reach the final render.

+-----------+
        | isLoading |--yes--> render <Spinner />
        +-----------+
              | no
              v
        +-----------+
        |   error   |--yes--> render <ErrorMessage />
        +-----------+
              | no
              v
        +-----------+
        | items = []|--yes--> render <EmptyState />
        +-----------+
              | no
              v
        render actual content

Important Rules

Use a ternary for small, inline either/or JSX choices directly inside a return statement.

Use && only when the left side is already boolean or explicitly coerced (count > 0), to avoid rendering a stray 0 or empty string.

Prefer early returns (guard clauses) once a component has more than two or three mutually exclusive states, such as loading/error/empty/success.

Order guard clauses so error and loading are checked before assuming data is present.

Gate permission-sensitive UI (admin controls, edit buttons, sensitive data) behind explicit boolean checks, ideally with an early return rather than deeply nested conditionals.

Where this shows up

Nearly every screen that fetches data — a dashboard, a product list, a user profile — needs loading, error, empty, and success states, and getting the order and boundaries of those conditions right is what separates a robust UI from one that flashes broken content. Permission-based conditional rendering is equally common in admin panels and multi-role apps, where the same page renders different controls for a viewer versus an editor versus an admin.

{count && <span>{count} new messages</span>} {count > 0 && <span>{count} new messages</span>}

When count is 0, {count && ...} evaluates to 0, and React renders that literal 0 on the page since 0 is a valid displayable value, unlike null or false.

if (items.length === 0) return <EmptyState />; if (error) return <ErrorMessage />;

if (error) return <ErrorMessage />; if (items.length === 0) return <EmptyState />;

Checking the empty-array case before the error case can show a misleading 'no items' message when the real issue is that the data request failed and items was never populated.

[multiple-choice] What does {unreadCount && <Badge />} render when unreadCount is 0? A. A literal 0 on the page B. Nothing at all C. The <Badge /> component D. A TypeScript compile error

A. A literal 0 on the page — Because 0 is falsy but still a renderable value, {0 && <Badge />} evaluates to 0, and React displays that 0 as text on the page — a common bug fixed by writing unreadCount > 0 instead.

[multiple-choice] In a data-fetching component with loading, error, empty, and success states, what is the recommended check order? A. success, empty, error, loading B. loading, error, empty, success C. empty, loading, success, error D. Order never matters

B. loading, error, empty, success — Checking loading first avoids inspecting data that isn't ready yet; checking error next avoids treating a failed request as 'no data'; checking empty next handles a successful-but-empty result; only then is it safe to render real content.

[true-false] True or false: null, undefined, and false all render as nothing in JSX. A. True B. False

A. True — True. React treats null, undefined, and false as producing no output, which is exactly what makes patterns like condition && <Element /> work for conditionally showing or hiding UI.

Write a component UserBadge({ role }: { role: 'admin' | 'member' | null }) that renders 'Administrator' for role 'admin', 'Member' for role 'member', and nothing at all when role is null.

function UserBadge({ role }: { role: 'admin' | 'member' | null }) { if (role === null) return null; return <span>{role === 'admin' ? 'Administrator' : 'Member'}</span>; }

You can chain a ternary, or use an early return of null for the null case before rendering the rest.