Conditional Rendering Patterns

Difficulty: Beginner

Question

What are the different patterns for conditional rendering in React? What are the pitfalls?

Answer

Conditional rendering is one of those things that every React developer does constantly, but there are several patterns to choose from, each with their own trade-offs and pitfalls. Let me walk you through all of them, including the one bug that catches almost everyone at least once.

The simplest way to think about conditional rendering in React is this: since JSX is just JavaScript expressions, you can use any JavaScript technique for controlling which elements appear. There's no special React directive like v-if or *ngIf. It's just plain JS logic. That said, some patterns work better than others in JSX.

Pattern 1: The logical AND operator (&&). This is the go-to for simple show/hide logic. {isLoggedIn && <Dashboard />} renders the Dashboard when isLoggedIn is true, and renders nothing when it's false. It's concise and readable. You'll use this hundreds of times in any React project.

But here's the infamous gotcha that catches everyone. What happens with this code: {items.length && <ItemList items={items} />}? When items has 3 elements, items.length is 3, which is truthy, so it renders ItemList. Great. But when items is empty, items.length is 0. And here's the thing: 0 is falsy in JavaScript, so the && expression evaluates to 0. But unlike null, undefined, true, or false (which React skips when rendering), the number 0 is a valid renderable value. React will literally render the text '0' on your screen. You'll see a random zero hanging out in your UI, and if you don't know about this pitfall, you'll spend an embarrassing amount of time trying to figure out where it came from.

The fix is simple: always make sure the left side of && is a boolean expression, not a number or other falsy-but-renderable value. Use {items.length > 0 && <ItemList />}, or {!!items.length && <ItemList />}, or {Boolean(items.length) && <ItemList />}. Any of these convert the number to a true boolean, which React won't render when it's false.

Same thing applies to empty strings. {someString && <Component />} will render an empty string (which is invisible but still present in the DOM) when someString is ''. Not usually a visible bug, but it adds unnecessary empty text nodes to your DOM.

Pattern 2: The ternary operator. Use this when you want to toggle between two things: {isLoggedIn ? <Dashboard /> : <LoginPage />}. This is perfect for either/or situations. However, ternaries can get ugly fast when you nest them. Three levels of nested ternaries is borderline unreadable. More than that, and you should refactor.

Pattern 3: Early returns (guard clauses). This is my personal favorite for handling loading, error, and auth states at the component level. Instead of wrapping your entire component's JSX in conditions, you check for edge cases at the top and return early. Something like: if (isLoading) return <Spinner />; if (error) return <ErrorMessage />; if (!user) return <LoginPrompt />; And then the rest of the component handles the happy path without any nesting. This keeps your main JSX clean and flat.

Early returns also have a practical benefit: they prevent accessing data that might not exist yet. If you check for loading before trying to render user.name, you avoid the 'cannot read property of undefined' error. Guard clauses serve as both conditional rendering AND error prevention.

Pattern 4: Object mapping for multi-way branching. When you have multiple conditions that determine which component to render (like status badges: active, pending, inactive, banned), don't use nested ternaries. Instead, create an object that maps each status to its configuration. Something like STATUS_CONFIG = { active: { label: 'Active', color: 'green' }, pending: { label: 'Pending', color: 'yellow' } } and then look up config[status]. This is clean, extensible (adding a new status is just adding a new key), and you can even put it outside the component for reuse.

Pattern 5: Helper functions or extracted components. When the conditional logic is complex (multiple conditions, nested branching, side effects), extract it into a function. A function called renderContent that uses a switch statement or if-else chain is often clearer than trying to squeeze complex logic into JSX with ternaries.

Pattern 6: Wrapper components for declarative conditionals. You can create components like <ShowWhen condition={isAdmin}> or <FeatureFlag name="betaSearch"> that encapsulate the condition. This is especially nice for feature flags, where you want to clearly mark which parts of the UI are behind a flag. It also makes the JSX more self-documenting.

Some things React simply doesn't render (they're 'transparent'): null, undefined, true, false, and empty strings. You can use this to your advantage. Returning null from a component effectively hides it. And true/false being invisible is what makes the && pattern work in the first place.

A few real-world tips:

- For permission-based rendering (show the delete button only for admins), create a utility component or hook that encapsulates the permission check. Don't scatter role checks all over your JSX.

- For A/B testing, wrap variants in conditional components that decide which version to show based on the user's experiment group.

- Remember that conditional rendering affects the component lifecycle. If {showForm && <Form />} toggles the form off and on, the Form component unmounts and remounts, losing all its state. If you want to preserve state, use CSS display:none instead of conditional rendering.

In interviews, definitely mention the 0 pitfall with && (it's practically a guaranteed follow-up question), show that you know early returns for guard clauses, and demonstrate that you prefer object mapping over nested ternaries for multi-way branching.

Code examples

Common Patterns and Pitfalls

function Dashboard({ user, notifications, isLoading, error }) {
  // Pattern 1: Early return (guard clauses)
  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <LoginPrompt />;

  // Pattern 2: && operator (show/hide)
  // GOOD:
  {user.isAdmin && <AdminPanel />}
  {notifications.length > 0 && <NotificationBadge count={notifications.length} />}

  // BAD: falsy value pitfall
  {notifications.length && <NotificationBadge />}
  // When length = 0, renders "0" on screen!

  // FIX: always use boolean expressions
  {notifications.length > 0 && <NotificationBadge />}
  {!!notifications.length && <NotificationBadge />}
  {Boolean(notifications.length) && <NotificationBadge />}

  // Pattern 3: Ternary (if-else)
  return (
    <div>
      {user.isAdmin ? <AdminView /> : <UserView />}
      <p>Status: {user.isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}

Early returns handle loading/error states cleanly. The && operator is great for show/hide but watch out for 0 and NaN values. Ternary handles two-way branching.

Complex Conditions with Mapping

// BAD: Nested ternaries are unreadable
function StatusBadge({ status }) {
  return (
    <span>
      {status === 'active'
        ? 'Active'
        : status === 'pending'
          ? 'Pending'
          : status === 'inactive'
            ? 'Inactive'
            : 'Unknown'}
    </span>
  );
}

// GOOD: Object mapping
const STATUS_CONFIG = {
  active: { label: 'Active', color: 'green' },
  pending: { label: 'Pending', color: 'yellow' },
  inactive: { label: 'Inactive', color: 'gray' },
  banned: { label: 'Banned', color: 'red' },
};

function StatusBadge({ status }) {
  const config = STATUS_CONFIG[status] || { label: 'Unknown', color: 'gray' };
  return (
    <span style={{ color: config.color }}>
      {config.label}
    </span>
  );
}

// GOOD: Helper function for complex logic
function NotificationIcon({ type, count }) {
  function renderIcon() {
    switch (type) {
      case 'message': return <MessageIcon count={count} />;
      case 'alert': return <AlertIcon />;
      case 'update': return <UpdateIcon />;
      default: return <BellIcon />;
    }
  }

  return <div className="notification">{renderIcon()}</div>;
}

Object mapping replaces nested ternaries with clean, extensible configuration. Helper functions encapsulate complex branching logic and keep JSX readable.

Conditional Rendering with Components

// Pattern: Render props for conditional logic
function ShowWhen({ condition, fallback = null, children }) {
  return condition ? children : fallback;
}

function App({ user }) {
  return (
    <div>
      <ShowWhen condition={user} fallback={<LoginPage />}>
        <Dashboard user={user} />
      </ShowWhen>

      <ShowWhen condition={user?.isAdmin}>
        <AdminPanel />
      </ShowWhen>
    </div>
  );
}

// Pattern: Feature flags
const FEATURES = {
  newDashboard: true,
  betaSearch: false,
  darkMode: true,
};

function FeatureFlag({ name, children, fallback = null }) {
  return FEATURES[name] ? children : fallback;
}

function App() {
  return (
    <div>
      <FeatureFlag name="newDashboard" fallback={<OldDashboard />}>
        <NewDashboard />
      </FeatureFlag>
      <FeatureFlag name="betaSearch">
        <BetaSearchBar />
      </FeatureFlag>
    </div>
  );
}

Wrapper components like ShowWhen and FeatureFlag make conditional logic declarative and reusable. They are especially useful for feature flags and permission-based rendering.

Key points

Concepts covered

Conditional Rendering, Ternary, Short-circuit, Pattern Matching