Episode 2 — React Frontend Architecture NextJS / 2.5 — Event Handling and Conditional Rendering

2.5.c — Conditional Rendering Patterns

In one sentence: Conditional rendering in React lets you show, hide, or swap UI elements based on state, props, or computed values using plain JavaScript expressions like &&, ternary operators, early returns, and object lookups — no special template syntax needed.

Navigation: ← Controlled Inputs · Next → Dynamic UI Rendering


Table of Contents

  1. What is Conditional Rendering?
  2. Pattern 1: The && Operator
  3. The && Gotcha — Rendering 0 and Other Falsy Values
  4. Pattern 2: Ternary Operator
  5. Pattern 3: Early Return
  6. Pattern 4: if/else Statements (Outside JSX)
  7. Pattern 5: switch Statement
  8. Pattern 6: Object Lookup (Map Pattern)
  9. Pattern 7: IIFE (Immediately Invoked Function Expression)
  10. Combining Patterns
  11. Conditional Rendering vs CSS Display/Visibility
  12. Rendering null vs undefined vs false
  13. Conditional Props and Attributes
  14. Real-World Patterns
  15. Decision Flowchart — Which Pattern to Use
  16. Common Mistakes
  17. Key Takeaways

1. What is Conditional Rendering?

Conditional rendering means showing different UI based on conditions — just like if statements control code flow, conditional rendering controls what appears on screen.

┌──────────────────────────────────────────────┐
│              Component State                  │
│                                                │
│  isLoggedIn: true                             │
│  role: "admin"                                │
│  hasErrors: false                             │
│  items: [...]                                 │
│       │                                        │
│       ▼                                        │
│  ┌─────────────────────────────────┐          │
│  │ Conditional Rendering Logic      │          │
│  │                                   │          │
│  │ isLoggedIn? → <Dashboard />      │          │
│  │ role=admin? → <AdminPanel />     │          │
│  │ hasErrors?  → <ErrorBanner />    │          │
│  │ items.length? → <ItemList />     │          │
│  └─────────────────────────────────┘          │
│       │                                        │
│       ▼                                        │
│  Final JSX output (only truthy branches)      │
└──────────────────────────────────────────────┘

React doesn't have a built-in v-if (Vue) or *ngIf (Angular). Instead, you use plain JavaScript inside JSX. This is a feature, not a limitation — you already know JavaScript.


2. Pattern 1: The && Operator

The && operator renders the right side only when the left side is truthy.

Basic Usage

function Notifications({ count }) {
  return (
    <div>
      <h1>Dashboard</h1>
      {count > 0 && <span className="badge">{count} new notifications</span>}
    </div>
  );
}

How It Works

// JavaScript short-circuit evaluation:
// If left side is falsy → return left side (short-circuit)
// If left side is truthy → return right side

true && 'Hello'    // → 'Hello' (rendered)
false && 'Hello'   // → false (React renders nothing for false)

// In JSX:
{isLoggedIn && <UserMenu />}
// isLoggedIn = true  → renders <UserMenu />
// isLoggedIn = false → renders nothing (false is invisible in JSX)

Multiple Conditions

function UserProfile({ user, isAdmin }) {
  return (
    <div>
      <h1>{user.name}</h1>
      {user.bio && <p>{user.bio}</p>}
      {user.avatar && <img src={user.avatar} alt={user.name} />}
      {isAdmin && <button>Edit User</button>}
      {user.email && user.isVerified && <span>✓ Verified</span>}
    </div>
  );
}

When to Use &&

Show something or nothing — binary show/hide ✅ The condition is simple (one boolean/comparison) ✅ There's no "else" case

Don't use when you have an else case (use ternary instead) ❌ Don't use with numbers that could be 0 (see gotcha below)


3. The && Gotcha — Rendering 0 and Other Falsy Values

This is the #1 most common conditional rendering bug in React.

The Bug

function MessageCount({ count }) {
  return (
    <div>
      {/* ❌ BUG: When count is 0, it renders "0" on screen */}
      {count && <span>{count} messages</span>}
    </div>
  );
}

// count = 5  → renders <span>5 messages</span> ✅
// count = 0  → renders "0" on screen ❌ (should render nothing!)

Why This Happens

// JavaScript falsy values:
// false, 0, -0, 0n, "", null, undefined, NaN

// In JSX rendering:
// false  → renders nothing ✅
// null   → renders nothing ✅
// undefined → renders nothing ✅
// ""     → renders nothing ✅
// 0      → renders "0" ❌ (React renders numbers!)
// NaN    → renders "NaN" ❌

// So:
0 && <span>messages</span>
// Returns 0 (left side)
// React renders "0" as text!

The Fixes

// Fix 1: Convert to boolean
{count > 0 && <span>{count} messages</span>}

// Fix 2: Double negation
{!!count && <span>{count} messages</span>}

// Fix 3: Explicit comparison
{count !== 0 && <span>{count} messages</span>}

// Fix 4: Ternary (most explicit)
{count ? <span>{count} messages</span> : null}

// Fix 5: Boolean() function
{Boolean(count) && <span>{count} messages</span>}

Falsy Values Rendering Cheat Sheet

ValueJSX OutputVisible?
falseNothingNo
nullNothingNo
undefinedNothingNo
""NothingNo
0"0"Yes ⚠️
NaN"NaN"Yes ⚠️
trueNothingNo
[] (empty array)NothingNo

4. Pattern 2: Ternary Operator

Use ternary when you have two possible outputs — an if/else in JSX.

Basic Usage

function LoginButton({ isLoggedIn }) {
  return (
    <button>
      {isLoggedIn ? 'Log Out' : 'Log In'}
    </button>
  );
}

Rendering Different Components

function AuthPage({ isLoggedIn }) {
  return (
    <main>
      {isLoggedIn ? <Dashboard /> : <LoginForm />}
    </main>
  );
}

Nested Ternaries (Use Sparingly)

function StatusMessage({ status }) {
  return (
    <p>
      {status === 'loading'
        ? 'Loading...'
        : status === 'error'
          ? 'Something went wrong'
          : status === 'success'
            ? 'Done!'
            : 'Unknown status'
      }
    </p>
  );
}
// ⚠️ This is hard to read. Use object lookup or switch instead.

Ternary for Conditional Props

function Alert({ type, message }) {
  return (
    <div
      style={{
        padding: 16,
        borderRadius: 8,
        background: type === 'error' ? '#fee' : type === 'success' ? '#efe' : '#eef',
        color: type === 'error' ? '#c00' : type === 'success' ? '#0a0' : '#00c',
        border: `1px solid ${type === 'error' ? '#fcc' : type === 'success' ? '#cfc' : '#ccf'}`,
      }}
    >
      {type === 'error' ? '❌' : type === 'success' ? '✅' : 'ℹ️'} {message}
    </div>
  );
}

When to Use Ternary

Show A or B — exactly two options ✅ Inline within JSX ✅ Short expressions on each side

Don't use when nesting more than 2 levels deep ❌ Don't use when each branch is complex (extract to variables or functions)


5. Pattern 3: Early Return

Return early from the component to skip rendering entirely.

Basic Usage

function UserProfile({ user }) {
  // Guard clause — don't render if no user
  if (!user) {
    return null;
  }

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

Multiple Guards

function ProtectedContent({ user, isLoading, error }) {
  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (error) {
    return <ErrorMessage message={error.message} />;
  }

  if (!user) {
    return <LoginPrompt />;
  }

  if (!user.hasAccess) {
    return <AccessDenied />;
  }

  // Happy path — only reached if all checks pass
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <SecretContent />
    </div>
  );
}

Why Early Returns are Powerful

┌─────────────────────────────────────┐
│ Without early return (nested mess): │
│                                     │
│ if (isLoading) {                    │
│   return <Spinner />                │
│ } else {                            │
│   if (error) {                      │
│     return <Error />                │
│   } else {                          │
│     if (!user) {                    │
│       return <Login />              │
│     } else {                        │
│       return <Content />   ← deeply │
│     }                        nested │
│   }                                 │
│ }                                   │
├─────────────────────────────────────┤
│ With early returns (flat):          │
│                                     │
│ if (isLoading) return <Spinner />   │
│ if (error) return <Error />         │
│ if (!user) return <Login />         │
│ return <Content />   ← clean!      │
└─────────────────────────────────────┘

When to Use Early Return

Loading / Error / Empty states at the top of a component ✅ Guard clauses for invalid data ✅ When the main JSX is complex and you want to simplify it

Don't use for small inline conditions (use && or ternary)


6. Pattern 4: if/else Statements (Outside JSX)

You can't use if/else inside JSX directly. But you can assign JSX to variables before the return.

function Greeting({ timeOfDay }) {
  let message;
  let icon;

  if (timeOfDay === 'morning') {
    message = 'Good morning!';
    icon = '🌅';
  } else if (timeOfDay === 'afternoon') {
    message = 'Good afternoon!';
    icon = '☀️';
  } else if (timeOfDay === 'evening') {
    message = 'Good evening!';
    icon = '🌙';
  } else {
    message = 'Hello!';
    icon = '👋';
  }

  return (
    <h1>{icon} {message}</h1>
  );
}

With Complex JSX

function OrderStatus({ status }) {
  let content;

  if (status === 'pending') {
    content = (
      <div className="status-pending">
        <Spinner size="small" />
        <p>Your order is being processed...</p>
        <p>Estimated: 2-3 business days</p>
      </div>
    );
  } else if (status === 'shipped') {
    content = (
      <div className="status-shipped">
        <TruckIcon />
        <p>Your order is on its way!</p>
        <a href="/tracking">Track shipment →</a>
      </div>
    );
  } else if (status === 'delivered') {
    content = (
      <div className="status-delivered">
        <CheckIcon />
        <p>Delivered! Enjoy your purchase.</p>
        <button>Leave a review</button>
      </div>
    );
  }

  return <div className="order-status">{content}</div>;
}

7. Pattern 5: switch Statement

function StatusBadge({ status }) {
  switch (status) {
    case 'active':
      return <span className="badge badge-green">Active</span>;
    case 'pending':
      return <span className="badge badge-yellow">Pending</span>;
    case 'suspended':
      return <span className="badge badge-red">Suspended</span>;
    case 'cancelled':
      return <span className="badge badge-gray">Cancelled</span>;
    default:
      return <span className="badge">Unknown</span>;
  }
}

Switch with Complex Rendering

function WizardStep({ step }) {
  function renderStep() {
    switch (step) {
      case 1:
        return <PersonalInfoForm />;
      case 2:
        return <AddressForm />;
      case 3:
        return <PaymentForm />;
      case 4:
        return <ReviewOrder />;
      default:
        return <NotFound />;
    }
  }

  return (
    <div className="wizard">
      <StepIndicator current={step} total={4} />
      <div className="wizard-content">
        {renderStep()}
      </div>
    </div>
  );
}

8. Pattern 6: Object Lookup (Map Pattern)

The most elegant pattern for mapping values to components or JSX.

Basic Lookup

function StatusIcon({ status }) {
  const iconMap = {
    success: '✅',
    error: '❌',
    warning: '⚠️',
    info: 'ℹ️',
  };

  return <span>{iconMap[status] || '❓'}</span>;
}

Component Lookup

function DynamicPage({ page }) {
  const pageComponents = {
    home: <HomePage />,
    about: <AboutPage />,
    contact: <ContactPage />,
    settings: <SettingsPage />,
  };

  return pageComponents[page] || <NotFoundPage />;
}

Styled Lookup (Alert Component)

function Alert({ type, message, onClose }) {
  const styles = {
    success: { bg: '#d4edda', color: '#155724', border: '#c3e6cb', icon: '✅' },
    error:   { bg: '#f8d7da', color: '#721c24', border: '#f5c6cb', icon: '❌' },
    warning: { bg: '#fff3cd', color: '#856404', border: '#ffeeba', icon: '⚠️' },
    info:    { bg: '#d1ecf1', color: '#0c5460', border: '#bee5eb', icon: 'ℹ️' },
  };

  const s = styles[type] || styles.info;

  return (
    <div style={{
      background: s.bg,
      color: s.color,
      border: `1px solid ${s.border}`,
      padding: '12px 16px',
      borderRadius: 8,
      display: 'flex',
      alignItems: 'center',
      gap: 8,
    }}>
      <span>{s.icon}</span>
      <span style={{ flex: 1 }}>{message}</span>
      {onClose && <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>×</button>}
    </div>
  );
}

Why Object Lookup is Superior to Nested Ternaries

// ❌ Nested ternary — hard to read and maintain
const color = status === 'active' ? 'green'
  : status === 'pending' ? 'yellow'
  : status === 'error' ? 'red'
  : 'gray';

// ✅ Object lookup — clean, extensible
const colorMap = {
  active: 'green',
  pending: 'yellow',
  error: 'red',
};
const color = colorMap[status] || 'gray';

9. Pattern 7: IIFE (Immediately Invoked Function Expression)

When you need complex logic inside JSX but don't want to extract a function:

function ComplexRender({ user }) {
  return (
    <div>
      <h1>Profile</h1>
      {(() => {
        if (!user) return <p>No user</p>;
        if (user.role === 'admin') return <AdminView user={user} />;
        if (user.role === 'editor') return <EditorView user={user} />;
        return <UserView user={user} />;
      })()}
    </div>
  );
}

Use this sparingly. Usually it's cleaner to extract to a named function or separate component.


10. Combining Patterns

Real components mix multiple patterns:

function UserDashboard({ user, notifications, isLoading, error }) {
  // Pattern: Early return for loading/error states
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorFallback error={error} />;
  if (!user) return <LoginRedirect />;

  // Pattern: Object lookup for role-based header
  const headerComponent = {
    admin: <AdminHeader />,
    editor: <EditorHeader />,
    viewer: <ViewerHeader />,
  };

  return (
    <div className="dashboard">
      {/* Pattern: Object lookup */}
      {headerComponent[user.role] || <DefaultHeader />}

      {/* Pattern: && for optional sections */}
      {user.isPremium && <PremiumBanner />}

      {/* Pattern: Ternary for binary toggle */}
      {notifications.length > 0
        ? <NotificationList items={notifications} />
        : <p>No new notifications</p>
      }

      {/* Pattern: && with safe number check */}
      {user.unreadCount > 0 && (
        <span className="badge">{user.unreadCount}</span>
      )}
    </div>
  );
}

11. Conditional Rendering vs CSS Display/Visibility

React Conditional (Unmounts Component)

{isVisible && <Modal />}
// When isVisible = false:
// - Component is REMOVED from the DOM
// - useEffect cleanup runs
// - State is lost
// - Event listeners are removed

CSS Hidden (Keeps in DOM)

<div style={{ display: showSidebar ? 'block' : 'none' }}>
  <Sidebar />
</div>
// When showSidebar = false:
// - Component stays in DOM (just invisible)
// - useEffect does NOT re-run
// - State is PRESERVED
// - Still uses memory

CSS Visibility (Keeps Space)

<div style={{ visibility: isVisible ? 'visible' : 'hidden' }}>
  <Widget />
</div>
// When isVisible = false:
// - Component stays in DOM
// - Still takes up SPACE (invisible but layout preserved)
// - No re-mount cost when toggling

When to Use Which

ApproachMounts/UnmountsPreserves StateTakes SpaceBest For
{show && <X />}YesNoNoModals, conditional sections
display: noneNoYesNoTabs, sidebars (keep state)
visibility: hiddenNoYesYesTooltips, placeholders
opacity: 0NoYesYesAnimation targets

12. Rendering null vs undefined vs false

React treats these values as "render nothing":

function InvisibleValues() {
  return (
    <div>
      {null}        {/* Renders nothing */}
      {undefined}   {/* Renders nothing */}
      {false}       {/* Renders nothing */}
      {true}        {/* Renders nothing */}
      {''}          {/* Renders nothing (empty string) */}
      {[]}          {/* Renders nothing (empty array) */}

      {0}           {/* ⚠️ Renders "0" */}
      {NaN}         {/* ⚠️ Renders "NaN" */}
    </div>
  );
}

Returning null from a Component

function ConditionalWrapper({ show, children }) {
  if (!show) {
    return null; // Component exists but renders nothing
  }

  return <div className="wrapper">{children}</div>;
}

13. Conditional Props and Attributes

Conditional className

function Button({ variant, isDisabled, size }) {
  // Template literal approach
  const className = `btn btn-${variant} ${isDisabled ? 'btn-disabled' : ''} ${size === 'large' ? 'btn-lg' : ''}`.trim();

  // Array filter approach (cleaner)
  const className2 = [
    'btn',
    `btn-${variant}`,
    isDisabled && 'btn-disabled',
    size === 'large' && 'btn-lg',
  ].filter(Boolean).join(' ');

  return <button className={className2} disabled={isDisabled}>Click</button>;
}

Conditional Spread Props

function Input({ error, ...rest }) {
  const errorProps = error ? {
    'aria-invalid': true,
    'aria-errormessage': `${rest.id}-error`,
    style: { borderColor: 'red' },
  } : {};

  return <input {...rest} {...errorProps} />;
}

Conditional Event Handlers

function InteractiveCard({ isEditable, onEdit }) {
  return (
    <div
      // Only attach handler if editable
      {...(isEditable && { onClick: onEdit })}
      style={{ cursor: isEditable ? 'pointer' : 'default' }}
    >
      Card Content
    </div>
  );
}

14. Real-World Patterns

Loading → Error → Empty → Data Pattern

function DataList({ data, isLoading, error }) {
  if (isLoading) {
    return (
      <div className="skeleton">
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="skeleton-row" />
        ))}
      </div>
    );
  }

  if (error) {
    return (
      <div className="error-state">
        <p>Failed to load data</p>
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  if (!data || data.length === 0) {
    return (
      <div className="empty-state">
        <img src="/empty.svg" alt="No data" />
        <p>Nothing here yet</p>
        <button>Create your first item</button>
      </div>
    );
  }

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Role-Based Navigation

function Navigation({ user }) {
  const navItems = [
    { label: 'Home', path: '/', roles: ['viewer', 'editor', 'admin'] },
    { label: 'Dashboard', path: '/dashboard', roles: ['editor', 'admin'] },
    { label: 'Reports', path: '/reports', roles: ['editor', 'admin'] },
    { label: 'Users', path: '/users', roles: ['admin'] },
    { label: 'Settings', path: '/settings', roles: ['admin'] },
  ];

  const visibleItems = navItems.filter(item =>
    item.roles.includes(user.role)
  );

  return (
    <nav>
      {visibleItems.map(item => (
        <a key={item.path} href={item.path}>{item.label}</a>
      ))}
    </nav>
  );
}

Feature Flags

function App({ featureFlags }) {
  return (
    <div>
      <Header />

      {featureFlags.newDashboard ? <NewDashboard /> : <OldDashboard />}

      {featureFlags.chatWidget && <ChatWidget />}

      {featureFlags.betaBanner && (
        <div className="beta-banner">
          🧪 You're using a beta feature!
        </div>
      )}
    </div>
  );
}

15. Decision Flowchart — Which Pattern to Use

Start: What do you need to render conditionally?
│
├── Show something OR nothing?
│   └── Use: {condition && <Component />}
│       ⚠️ Watch for 0/NaN — use condition > 0 or Boolean(condition)
│
├── Show A or B (two options)?
│   ├── Both are short/simple?
│   │   └── Use: {condition ? <A /> : <B />}
│   └── One or both are complex?
│       └── Use: if/else before return, assign to variable
│
├── Multiple options (3+)?
│   ├── Based on a single value (status, role, page)?
│   │   ├── Values are known at compile time?
│   │   │   └── Use: Object lookup map
│   │   └── Complex logic per case?
│   │       └── Use: switch statement
│   └── Based on multiple independent conditions?
│       └── Combine: early returns + && + ternary
│
└── Loading/Error/Empty guard?
    └── Use: Early returns at the top of the component
PatternBest ForLines of Code
&&Show/hide one element1
Ternary ? :Toggle between 2 options1
Early returnLoading/error/empty guards1-3 each
if/else variable2-3 complex branches5-15
switch4+ branches on one value10-20
Object lookup4+ branches, config-driven5-10

16. Common Mistakes

Mistake 1: && with Potentially Falsy Numbers

// ❌ Renders "0" when items is empty
{items.length && <List items={items} />}

// ✅ Fix: Compare explicitly
{items.length > 0 && <List items={items} />}

Mistake 2: Forgetting the Fallback

// ❌ Renders nothing when status doesn't match
const content = {
  active: <ActiveView />,
  pending: <PendingView />,
};
return content[status]; // undefined if status = 'archived'

// ✅ Always provide a fallback
return content[status] || <DefaultView />;
// OR
return content[status] ?? <DefaultView />;

Mistake 3: Over-Nesting Ternaries

// ❌ Unreadable
{a ? b ? <X /> : <Y /> : c ? <Z /> : <W />}

// ✅ Extract to function or use early returns
function renderContent() {
  if (a && b) return <X />;
  if (a) return <Y />;
  if (c) return <Z />;
  return <W />;
}

Mistake 4: Conditional Hooks (Illegal)

// ❌ ILLEGAL — hooks must be called unconditionally
function Bad({ showCounter }) {
  if (showCounter) {
    const [count, setCount] = useState(0); // BREAKS HOOK RULES
  }
  // ...
}

// ✅ CORRECT — always call the hook, conditionally render
function Good({ showCounter }) {
  const [count, setCount] = useState(0); // Always called
  return showCounter ? <p>Count: {count}</p> : null;
}

17. Key Takeaways

  1. React uses plain JavaScript for conditional rendering — no special directives
  2. && = show or nothing. Watch for the 0 gotcha.
  3. Ternary = show A or B. Keep it to one level of nesting.
  4. Early return = best for loading/error/empty guards at the top of components
  5. Object lookup = cleanest for mapping a value to different components/styles
  6. switch = good for complex multi-branch logic
  7. Conditional rendering unmounts the component (state is lost). CSS hiding preserves state.
  8. null, undefined, false, true, "" render nothing. 0 and NaN render as text.
  9. Don't nest ternaries more than 2 levels — extract to functions or use lookup patterns
  10. Never call hooks conditionally — always call hooks, conditionally render the output

Explain-It Challenge

  1. The Traffic Light Analogy: Explain the difference between &&, ternary, and early return using the analogy of a traffic light system. When does each "pattern" naturally apply?

  2. The Bug Hunter: A developer shows you {users.length && <UserList users={users} />} and says it renders "0" when there are no users. Explain exactly why this happens at the JavaScript level and give three different fixes.

  3. The Architecture Review: You're reviewing a component with 5 nested ternary operators. Explain to the developer why this is problematic and refactor it using at least two alternative patterns.


Navigation: ← Controlled Inputs · Next → Dynamic UI Rendering