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
- What is Conditional Rendering?
- Pattern 1: The && Operator
- The && Gotcha — Rendering 0 and Other Falsy Values
- Pattern 2: Ternary Operator
- Pattern 3: Early Return
- Pattern 4: if/else Statements (Outside JSX)
- Pattern 5: switch Statement
- Pattern 6: Object Lookup (Map Pattern)
- Pattern 7: IIFE (Immediately Invoked Function Expression)
- Combining Patterns
- Conditional Rendering vs CSS Display/Visibility
- Rendering null vs undefined vs false
- Conditional Props and Attributes
- Real-World Patterns
- Decision Flowchart — Which Pattern to Use
- Common Mistakes
- 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
| Value | JSX Output | Visible? |
|---|---|---|
false | Nothing | No |
null | Nothing | No |
undefined | Nothing | No |
"" | Nothing | No |
0 | "0" | Yes ⚠️ |
NaN | "NaN" | Yes ⚠️ |
true | Nothing | No |
[] (empty array) | Nothing | No |
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
| Approach | Mounts/Unmounts | Preserves State | Takes Space | Best For |
|---|---|---|---|---|
{show && <X />} | Yes | No | No | Modals, conditional sections |
display: none | No | Yes | No | Tabs, sidebars (keep state) |
visibility: hidden | No | Yes | Yes | Tooltips, placeholders |
opacity: 0 | No | Yes | Yes | Animation 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
| Pattern | Best For | Lines of Code |
|---|---|---|
&& | Show/hide one element | 1 |
Ternary ? : | Toggle between 2 options | 1 |
| Early return | Loading/error/empty guards | 1-3 each |
| if/else variable | 2-3 complex branches | 5-15 |
| switch | 4+ branches on one value | 10-20 |
| Object lookup | 4+ branches, config-driven | 5-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
- React uses plain JavaScript for conditional rendering — no special directives
&&= show or nothing. Watch for the0gotcha.- Ternary = show A or B. Keep it to one level of nesting.
- Early return = best for loading/error/empty guards at the top of components
- Object lookup = cleanest for mapping a value to different components/styles
- switch = good for complex multi-branch logic
- Conditional rendering unmounts the component (state is lost). CSS hiding preserves state.
null,undefined,false,true,""render nothing.0andNaNrender as text.- Don't nest ternaries more than 2 levels — extract to functions or use lookup patterns
- Never call hooks conditionally — always call hooks, conditionally render the output
Explain-It Challenge
-
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? -
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. -
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