Skip to content
C Codeloom
React

Conditional Rendering in React

A practical guide to conditional rendering in React — early returns, ternaries, the && shortcut and its zero pitfall, component maps, and the loading/error/empty pattern.

·9 min read · By Yash Kesharwani
Beginner 10 min read

What you'll learn

  • How to choose between if/early return, ternary, and the && shortcut
  • The falsy-zero pitfall that bites every React beginner
  • How to replace long if/else ladders with a component map
  • When returning null is the right call
  • The loading / error / empty / data pattern that production UIs use

Prerequisites

  • You understand components and JSX — see Components and JSX
  • Comfort with JavaScript expressions, truthy/falsy values, and ternaries

Real UIs are full of “show this only when…” decisions. A button that turns into a spinner. An empty state when a list has no items. A banner that appears only for admins. React does not give you a special syntax for any of this — it gives you JavaScript expressions inside { }. This post walks through the patterns you will use every day.

The core idea

JSX is JavaScript. Anything that returns an element (or null) is valid for React to render. That single fact powers every pattern in this post.

function Banner({ show }) {
  if (!show) return null;
  return <div className="banner">Sale ends tonight!</div>;
}

When a component returns null, React renders nothing — no DOM node, no whitespace. This is the simplest form of conditional rendering and often the cleanest.

Pattern 1: Early return

When a whole component should either render or not render based on a single condition, an early return is usually the right call.

function AdminTools({ user }) {
  if (!user?.isAdmin) return null;

  return (
    <div className="admin-tools">
      <button>Delete user</button>
      <button>Reset password</button>
    </div>
  );
}

The benefit is readability. The reader does not have to scan a long ternary to see “this only renders for admins” — it is right at the top.

Use early returns when the condition guards the entire output. Avoid them when only a small slice of the JSX changes; otherwise you end up duplicating the surrounding markup.

Pattern 2: Ternary inside JSX

When you want one of two values in the same spot, the ternary is the natural fit.

function Status({ online }) {
  return (
    <p className={online ? "ok" : "warn"}>
      {online ? "Online now" : "Offline"}
    </p>
  );
}

Ternaries shine for small either/or choices — a label, a className, an icon. They become painful when each branch contains several lines of JSX. At that point, lift the branches into variables:

function Greeting({ user }) {
  const message = user
    ? <p>Welcome back, {user.name}.</p>
    : <p>Please sign in.</p>;

  return <header>{message}</header>;
}

A function-local variable is cheap and keeps the returned JSX clean.

Pattern 3: The && shortcut

When there is no else branch, the logical AND is shorter than a ternary.

function Inbox({ unread }) {
  return (
    <div>
      <h2>Inbox</h2>
      {unread > 0 && <p>You have {unread} unread messages.</p>}
    </div>
  );
}

A && B evaluates to A when A is falsy and to B when A is truthy. React renders the result. When unread > 0 is true, it renders the <p>. When it is false, React renders nothing.

The falsy-zero pitfall

&& only behaves nicely when the left side is a boolean. With a raw number, you get a surprise.

function Cart({ items }) {
  return (
    <div>
      {items.length && <p>You have {items.length} items.</p>}
    </div>
  );
}
// output (when items.length is 0): the page shows "0"

0 && <p>…</p> is 0. React renders the number zero into the DOM. The same applies to "", NaN, and similar falsy non-boolean values.

The fix is to force the left side to be a real boolean:

{items.length > 0 && <p>You have {items.length} items.</p>}

Or use a ternary with null for the else branch:

{items.length ? <p>You have {items.length} items.</p> : null}

This is the single most common React rendering bug. Whenever the left side of && is a number that could legitimately be zero, reach for one of the fixes above.

Try it yourself. Build a small <Cart items={[]} /> component using {items.length && ...} and confirm the page shows a stray 0. Then switch to items.length > 0 && ... and confirm it disappears. Seeing the bug once makes it stick forever.

Pattern 4: Multiple branches with a component map

If/else ladders inside JSX get ugly fast.

function Icon({ kind }) {
  if (kind === "info") return <InfoIcon />;
  if (kind === "warn") return <WarnIcon />;
  if (kind === "error") return <ErrorIcon />;
  return <DefaultIcon />;
}

This works, but as the number of branches grows, a lookup object is cleaner and easier to extend:

const ICONS = {
  info: InfoIcon,
  warn: WarnIcon,
  error: ErrorIcon,
};

function Icon({ kind }) {
  const Component = ICONS[kind] ?? DefaultIcon;
  return <Component />;
}

A few things are happening. ICONS[kind] looks up a component by key. ?? falls back to DefaultIcon when the key is missing. And the capitalised Component lets JSX render the result — remember, JSX uses the case of the identifier to tell components apart from HTML tags.

This pattern scales much better than a switch statement and reads as data, not control flow.

Pattern 5: Returning null

A component is allowed to return null to render nothing.

function MaybeAvatar({ user }) {
  if (!user?.avatarUrl) return null;
  return <img src={user.avatarUrl} alt="" className="avatar" />;
}

null is not an error. It does not produce a warning. It is the idiomatic way to say “this component contributes nothing right now.” Prefer it over rendering an empty <div> or a fragment — both add nodes you do not want.

You can also return null from inside a ternary used in JSX:

{user ? <Profile user={user} /> : null}

That is equivalent to leaving the slot empty.

The loading / error / empty / data pattern

Most real components fetch data. Each fetch has four possible UI states, and forgetting one leads to “the page is just blank, what’s happening” bugs. The standard pattern handles all four explicitly.

import { useEffect, useState } from "react";

function PostList() {
  const [posts, setPosts] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch("/api/posts")
      .then((r) => r.json())
      .then(setPosts)
      .catch(setError);
  }, []);

  if (error) return <p className="error">Failed to load: {error.message}</p>;
  if (posts === null) return <p>Loading posts…</p>;
  if (posts.length === 0) return <p>No posts yet. Be the first to write one.</p>;

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

Read the early returns top to bottom — each one handles one state and exits. By the time the final return runs, you know posts is a non-empty array and there is no error. The reader does not have to keep state combinations in their head.

A few notes on the choices.

  • null for “not loaded yet” distinguishes “no data fetched yet” from [] (“fetched and the list is empty”). The empty array is a real result with its own UI.
  • Error first ensures you do not show a stale loading state when the request has already failed.
  • Empty state is not optional. A blank <ul> looks like a bug to users. Tell them what is going on.

If you find yourself with a fifth state (“loading more”), add another early return. The pattern scales.

Try it yourself. Take any existing list component you have and add all four branches — error, loading, empty, data. Force each case by mocking the fetch (throw, never resolve, resolve with [], resolve with items) and confirm every branch renders something sensible.

Inline conditional className

Conditional rendering is not only about whole elements. The same expressions work for attributes too.

function NavLink({ active, href, children }) {
  return (
    <a href={href} className={active ? "nav-link active" : "nav-link"}>
      {children}
    </a>
  );
}

For more than two classes, a small helper keeps the JSX tidy:

function cn(...parts) {
  return parts.filter(Boolean).join(" ");
}

<a className={cn("nav-link", active && "active", disabled && "disabled")} />

filter(Boolean) drops false, null, and undefined, so each condition && "class-name" either contributes a class or nothing. Libraries like clsx and classnames do the same thing with more features.

A small worked example

A notification dropdown that combines several of these patterns.

function Notifications({ notifications, loading, error }) {
  if (error) return <div className="error">Could not load notifications.</div>;
  if (loading) return <div className="muted">Loading…</div>;

  const unread = notifications.filter((n) => !n.read);

  return (
    <div className="notifications">
      <header>
        <h3>Notifications</h3>
        {unread.length > 0 && (
          <span className="badge">{unread.length} new</span>
        )}
      </header>

      {notifications.length === 0 ? (
        <p className="muted">You're all caught up.</p>
      ) : (
        <ul>
          {notifications.map((n) => (
            <li key={n.id} className={n.read ? "read" : "unread"}>
              {n.message}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Every pattern in this post shows up: early return for error and loading, a guarded && for the badge, a ternary for the empty state, and a conditional className on each item.

Recap

You now know:

  • A component returning null renders nothing
  • Early returns are best when a single condition guards the entire output
  • Ternaries are best for small either/or choices inside JSX
  • && is best for “render this only when true” — but force a boolean on the left side or you will render a stray 0
  • Replace long if/else ladders with a component map keyed by a string
  • Handle error, loading, empty, and data as four explicit branches in data-driven components
  • Conditional classNames use the same expressions — a small cn helper keeps them readable

Next steps

Conditional rendering decides whether to render. The next post handles how to render many things at once — rendering lists, the role of the key prop, and the subtle bugs that come from using array index as a key.

→ Next: Rendering Lists and Keys in React

Questions or feedback? Email codeloomdevv@gmail.com.