๐ Conditional Rendering Patterns
Not all components should render the same way all the time. Sometimes you show a login form, sometimes a user profile. Sometimes you display data, sometimes a loading spinner. Sometimes you show everything, sometimes just an error message. Conditional rendering is how you make your UI adapt to different states, data, and user actions. React gives you multiple ways to conditionally render content, and choosing the right pattern makes your code cleaner and easier to understand. Let's master all the patterns! ๐
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Use if/else statements for conditional rendering
- Apply ternary operators in JSX
- Use logical && and || operators effectively
- Implement switch statements for multiple conditions
- Use early returns and guard clauses
- Choose the right pattern for each scenario
- Handle loading, error, and empty states
- Avoid common conditional rendering pitfalls
- Write clean, readable conditional JSX
Estimated Time: 70-85 minutes
Project: Build a dashboard with multiple conditional sections
In This Lesson
๐ฏ Conditional Rendering Basics
Think of a React component as a smart picture frame: instead of always showing the same photo, it looks at what is going on right now and decides which picture to slot in. Logged in? Show the dashboard. Still fetching? Show a spinner. Nothing to display? Show a friendly empty message. That decision-making is conditional rendering, and the good news is it works exactly like the conditions you already know from plain JavaScript โ if, the ternary ?:, &&, ||, switch. You are not learning new React magic here; you are learning where each familiar JavaScript tool fits best inside JSX.
๐ Definition
Conditional Rendering: deciding what a component returns โ a different element, a different component, or nothing at all โ based on state, props, or any other data available at render time.
Why Conditional Rendering?
Real-World Examples
| Scenario | Condition | What to Render |
|---|---|---|
| User Authentication | Is user logged in? | Dashboard vs Login Form |
| Data Loading | Is data loaded? | Content vs Spinner |
| Permissions | Is user admin? | Admin Panel vs Normal View |
| Form Validation | Is form valid? | Submit Button vs Error Message |
| Feature Toggle | Is feature enabled? | New Feature vs Old Feature |
| Empty State | Is list empty? | Empty Message vs List Items |
The Basic Pattern
At its simplest, conditional rendering is one if that changes what the function returns. A React function component is just a function that returns JSX, so a plain if works exactly as you would expect:
Simple Example
function Greeting() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
// The condition decides which JSX we return
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please sign in</h1>;
}
๐ก A note on typing components in 2026 (React 19)
You will see two common styles for typing a component. The classic one uses React.FC<Props>. The style the React team and most modern codebases now favour is to type the props argument directly and let TypeScript infer the return type:
// Modern, recommended style โ plain function, props typed inline
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign in</h1>;
}
Both compile to the same thing. This lesson uses React.FC in several places because you will meet it constantly in existing code, but the inline-props style is perfectly good โ pick one and stay consistent. Either way, always use function components; class components are legacy.
Multiple Ways to Render Conditionally
๐ฏ Interactive: Which Pattern Should You Use?
Click through this decision tree to find the best conditional rendering pattern for your use case:
Each pattern has its own use case. Let's explore them all!
๐ If/Else Statements
The most straightforward way to render conditionally is the humble if/else. Because a component is a function, you can write ordinary statement-level logic before the return โ run calculations, check several conditions, log things, set breakpoints. This is the pattern to reach for whenever the logic is more than a one-liner, or when the two branches return completely different components. It reads top-to-bottom like a checklist, which is exactly how our brains debug.
Basic If/Else
Component-Level Conditionals
interface GreetingProps {
isLoggedIn: boolean;
}
const Greeting: React.FC<GreetingProps> = ({ isLoggedIn }) => {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please log in</h1>;
};
// Or with explicit else:
const Greeting: React.FC<GreetingProps> = ({ isLoggedIn }) => {
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
} else {
return <h1>Please log in</h1>;
}
};
Element Variables
Store JSX in Variables
const LoginControl: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
let button;
if (isLoggedIn) {
button = <button onClick={() => setIsLoggedIn(false)}>Logout</button>;
} else {
button = <button onClick={() => setIsLoggedIn(true)}>Login</button>;
}
return (
<div>
<h1>{isLoggedIn ? 'Welcome!' : 'Please sign in'}</h1>
{button}
</div>
);
};
If/Else If/Else Chain (Rendering by State)
A huge amount of real UI boils down to "which of these mutually-exclusive states am I in?" โ idle, loading, success, or error. Model that state as a discriminated union (a union of string literals), and TypeScript keeps you honest: it narrows the type inside each branch and warns you if a state has no home.
Multiple Conditions
type Status = 'idle' | 'loading' | 'success' | 'error';
interface DataDisplayProps {
status: Status;
data?: string[]; // real shape โ never `any`
error?: string;
}
const DataDisplay: React.FC<DataDisplayProps> = ({ status, data, error }) => {
if (status === 'loading') {
return <div>Loading…</div>;
} else if (status === 'error') {
return <div>Error: {error ?? 'Something went wrong'}</div>;
} else if (status === 'success') {
// Guard against rendering undefined: default to an empty array
return <ul>{(data ?? []).map((item) => <li key={item}>{item}</li>)}</ul>;
} else {
return <div>No data yet</div>;
}
};
โ ๏ธ Never reach for any here
The old version of this example typed data as any, which silently switches off type checking for everything you do with it. In 2026 TypeScript, prefer the real shape (string[], a proper interface, etc.). If you genuinely do not know the shape yet, use unknown and narrow it โ unknown forces you to check before you use it, whereas any lets bugs walk right through.
When to Use If/Else
| Use Case | Example |
|---|---|
| Complex conditions | Multiple checks needed |
| Early returns | Return different components entirely |
| Element variables | Calculate JSX before return |
| Readable logic | When inline operators would be messy |
โ Pro Tip
If/else is great when:
- You're returning completely different components
- The condition logic is complex
- You need to do calculations before rendering
โ Ternary Operator
The ternary operator โ condition ? valueIfTrue : valueIfFalse โ is the workhorse of inline conditional rendering. Because it is an expression (it evaluates to a value) rather than a statement, you can drop it straight into the middle of JSX where an if would be illegal. Reach for it when you have a clean either/or choice and both outcomes are short. Think of it as the "this or that" switch you embed right where the content goes.
Basic Ternary
Inline Conditionals
const Message: React.FC<{ isOnline: boolean }> = ({ isOnline }) => {
return (
<div>
{isOnline ? (
<span style={{ color: 'green' }}>โ Online</span>
) : (
<span style={{ color: 'gray' }}>โ Offline</span>
)}
</div>
);
};
// Or simpler:
const Status: React.FC<{ isActive: boolean }> = ({ isActive }) => (
<span>{isActive ? 'Active' : 'Inactive'}</span>
);
Ternary with Components
Rendering Different Components
const Dashboard: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
{isLoggedIn ? (
<UserDashboard />
) : (
<LoginForm />
)}
</div>
);
};
// With props:
const Greeting: React.FC<{ user: User | null }> = ({ user }) => (
<div>
{user ? (
<h1>Hello, {user.name}!</h1>
) : (
<h1>Hello, Guest!</h1>
)}
</div>
);
Nested Ternaries
โ ๏ธ Be Careful with Nested Ternaries
// โ Hard to read
const Status = ({ status }) => (
<div>
{status === 'loading' ? (
<Spinner />
) : status === 'error' ? (
<Error />
) : status === 'empty' ? (
<Empty />
) : (
<Content />
)}
</div>
);
// โ
Better: Use if/else or switch
const Status = ({ status }) => {
if (status === 'loading') return <Spinner />;
if (status === 'error') return <Error />;
if (status === 'empty') return <Empty />;
return <Content />;
};
Ternary for Class Names and Styles
Conditional Styling
const Button: React.FC<{ isPrimary: boolean }> = ({ isPrimary }) => (
<button
className={isPrimary ? 'btn-primary' : 'btn-secondary'}
style={{
backgroundColor: isPrimary ? '#667eea' : '#f5f5f5',
color: isPrimary ? 'white' : '#333'
}}
>
Click me
</button>
);
// Multiple conditional classes
const Card: React.FC<{ isActive: boolean; isHovered: boolean }> = ({
isActive,
isHovered
}) => (
<div
className={`card ${isActive ? 'active' : ''} ${isHovered ? 'hovered' : ''}`}
>
Content
</div>
);
When to Use Ternary
| Use When | Example |
|---|---|
| Inline in JSX | Choosing between two components |
| Simple conditions | Boolean check with two outcomes |
| Conditional props | Different values based on condition |
| Short expressions | Both outcomes are brief |
๐ก Ternary Best Practices
- โ Keep it simple - one level deep
- โ Use parentheses for clarity
- โ Format for readability
- โ Avoid nesting ternaries
- โ Don't use for complex logic
๐ฎ Interactive: Ternary Operator Visualizer
Toggle the condition to see how the ternary operator evaluates:
โ Logical && Operator
Reach for && when you want to show something only if a condition is true, with no "else" branch โ a badge, an alert, an extra section. It reads almost like English: "condition and then this element." The trick is understanding why it works, because that same mechanism hides a famous footgun with the number 0.
Basic && Usage
Render or Nothing
const Notification: React.FC = () => {
const [hasNewMessages, setHasNewMessages] = useState(true);
const [messageCount, setMessageCount] = useState(5);
return (
<div>
<h1>Inbox</h1>
{/* Only show if condition is true */}
{hasNewMessages && (
<div className="alert">
You have new messages!
</div>
)}
{/* Show message count if greater than 0 */}
{messageCount > 0 && (
<p>{messageCount} unread messages</p>
)}
</div>
);
};
How && Works in JSX
Understanding the Logic
// In JavaScript:
true && 'Hello' // Returns 'Hello'
false && 'Hello' // Returns false
// In JSX:
{true && <div>Hello</div>} // Renders <div>Hello</div>
{false && <div>Hello</div>} // Renders nothing
// React SKIPS: false, null, undefined, and true (renders nothing)
// React RENDERS: strings and numbers โ including 0 and NaN!
// That last line is the whole reason for the gotcha below.
Here is the mental model: a && b does not return true/false. It returns the first operand if it is falsy, otherwise the second operand. So 0 && <p/> evaluates to 0 โ and because React happily renders numbers, that 0 shows up on the page. The fix is to give && a real boolean on the left: write count > 0 && โฆ, or wrap the value with Boolean(count) && โฆ, or just use a ternary.
Common && Patterns
Practical Examples
const UserProfile: React.FC<{ user: User | null }> = ({ user }) => (
<div>
{/* Show only if user exists */}
{user && (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)}
{/* Show only if user is admin */}
{user && user.role === 'admin' && (
<button>Admin Panel</button>
)}
{/* Show only if user has premium */}
{user?.isPremium && (
<div className="premium-badge">โญ Premium</div>
)}
</div>
);
const TodoList: React.FC<{ todos: Todo[] }> = ({ todos }) => (
<div>
{/* Show only if there are todos */}
{todos.length > 0 && (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
{/* Show only if list is empty */}
{todos.length === 0 && (
<p>No todos yet!</p>
)}
</div>
);
โ ๏ธ Common Pitfall: Falsy Values
Watch Out for 0 and ''
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
// โ Problem: When count is 0, it renders "0" on screen!
return (
<div>
{count && <p>Count: {count}</p>}
</div>
);
// When count = 0, React renders the 0!
// โ
Solution: Explicit comparison
return (
<div>
{count > 0 && <p>Count: {count}</p>}
</div>
);
};
// Another example:
const Items: React.FC<{ items: string[] }> = ({ items }) => (
<div>
{/* โ Shows "0" when empty */}
{items.length && <p>{items.length} items</p>}
{/* โ
Correct */}
{items.length > 0 && <p>{items.length} items</p>}
</div>
);
๐ฎ Interactive: The && Falsy-Number Gotcha
Change count with the buttons and compare the two expressions side by side. Watch closely when count hits 0: the "buggy" line leaks a stray 0 onto the page, while the "fixed" line renders nothing.
count = 3
๐ Logical || Operator (OR)
The logical OR operator (||) is useful for providing fallback values or default content when something is null, undefined, or falsy.
Basic Pattern
Providing Fallbacks
interface UserProps {
user?: {
name?: string;
avatar?: string;
};
}
const UserDisplay: React.FC<UserProps> = ({ user }) => (
<div className="user-card">
{/* Show user name or fallback */}
<h2>{user?.name || 'Anonymous User'}</h2>
{/* Show avatar or default */}
<img
src={user?.avatar || '/default-avatar.png'}
alt="User avatar"
/>
</div>
);
// Real-world example: Settings with defaults
interface SettingsProps {
theme?: 'light' | 'dark';
language?: string;
fontSize?: number;
}
const Settings: React.FC<SettingsProps> = ({
theme,
language,
fontSize
}) => (
<div className={`theme-${theme || 'light'}`}>
<p>Language: {language || 'English'}</p>
<p>Font Size: {fontSize || 16}px</p>
</div>
);
Nullish Coalescing (??)
๐ก Modern Alternative
The nullish coalescing operator (??) is better than || when you want to treat 0, false, and empty strings as valid values:
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
// โ Problem with ||: 0 is considered falsy
return <p>Count: {count || 'No count'}</p>;
// Renders: "Count: No count" when count is 0!
// โ
Solution with ??: only null/undefined are replaced
return <p>Count: {count ?? 'No count'}</p>;
// Renders: "Count: 0" correctly!
};
// More examples:
const Examples: React.FC = () => {
const emptyString = '';
const zero = 0;
const falseValue = false;
return (
<div>
{/* With || */}
<p>{emptyString || 'default'}</p> {/* Shows: default */}
<p>{zero || 'default'}</p> {/* Shows: default */}
<p>{falseValue || 'default'}</p> {/* Shows: default */}
{/* With ?? */}
<p>{emptyString ?? 'default'}</p> {/* Shows: (empty) */}
<p>{zero ?? 'default'}</p> {/* Shows: 0 */}
<p>{falseValue ?? 'default'}</p> {/* Shows: false */}
</div>
);
};
Chaining || for Multiple Fallbacks
Multiple Default Options
interface DataDisplayProps {
primaryData?: string;
secondaryData?: string;
tertiaryData?: string;
}
const DataDisplay: React.FC<DataDisplayProps> = ({
primaryData,
secondaryData,
tertiaryData
}) => (
<div>
{/* Try multiple sources in order */}
<p>{primaryData || secondaryData || tertiaryData || 'No data available'}</p>
</div>
);
// Example with user profiles
interface ProfileProps {
displayName?: string;
username?: string;
email?: string;
}
const ProfileHeader: React.FC<ProfileProps> = ({
displayName,
username,
email
}) => (
<div className="profile-header">
<h1>{displayName || username || email?.split('@')[0] || 'User'}</h1>
</div>
);
โ Best Practice
Use || for default strings/objects, but use ?? when 0, false, or empty string are valid values you want to display.
๐ฎ Interactive: || vs ?? Comparison
Click different values to see how || and ?? behave differently:
value || 'default'
value ?? 'default'
๐ Switch Statements
When the thing you are branching on is a single value with several possible cases โ an order status, a user role, an alert type โ a switch reads more clearly than a long if/else if ladder. Even better, when you pair a switch with a TypeScript discriminated union, the compiler can prove you handled every case (more on that at the end of the lesson). And for the very common "same layout, different data" situation, an object map is often cleaner still.
Basic Switch Pattern
Status-Based Rendering
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
interface OrderProps {
status: OrderStatus;
orderId: string;
}
const OrderStatusDisplay: React.FC<OrderProps> = ({ status, orderId }) => {
// Helper function using switch
const getStatusDisplay = () => {
switch (status) {
case 'pending':
return (
<div className="status status-pending">
<h3>โณ Order Pending</h3>
<p>Waiting for confirmation...</p>
</div>
);
case 'processing':
return (
<div className="status status-processing">
<h3>๐ฆ Processing Order</h3>
<p>Your order is being prepared.</p>
</div>
);
case 'shipped':
return (
<div className="status status-shipped">
<h3>๐ Order Shipped</h3>
<p>Your package is on its way!</p>
</div>
);
case 'delivered':
return (
<div className="status status-delivered">
<h3>โ
Delivered</h3>
<p>Order #{orderId} has been delivered.</p>
</div>
);
case 'cancelled':
return (
<div className="status status-cancelled">
<h3>โ Order Cancelled</h3>
<p>This order has been cancelled.</p>
</div>
);
default:
return <p>Unknown status</p>;
}
};
return (
<div className="order-status">
{getStatusDisplay()}
</div>
);
};
Object Mapping Pattern (Better!)
๐ก More Elegant Approach
Instead of switch statements, you can use object mapping for cleaner, more maintainable code:
type AlertType = 'info' | 'success' | 'warning' | 'error';
interface AlertProps {
type: AlertType;
message: string;
}
const Alert: React.FC<AlertProps> = ({ type, message }) => {
// Map types to configurations
const alertConfig = {
info: {
icon: 'โน๏ธ',
className: 'alert-info',
title: 'Information'
},
success: {
icon: 'โ
',
className: 'alert-success',
title: 'Success'
},
warning: {
icon: 'โ ๏ธ',
className: 'alert-warning',
title: 'Warning'
},
error: {
icon: 'โ',
className: 'alert-error',
title: 'Error'
}
};
const config = alertConfig[type]; // fully type-safe: `type` is one of four keys
return (
<div className={`alert ${config.className}`}>
<span className="alert-icon">{config.icon}</span>
<div className="alert-content">
<h4>{config.title}</h4>
<p>{message}</p>
</div>
</div>
);
};
Want the compiler to guarantee your map covers every variant? Type it as a Record keyed by the union: const alertConfig: Record<AlertType, AlertConfig> = { โฆ }. Now if you add a fifth AlertType and forget to add its entry, TypeScript flags the object immediately โ the same safety net a never exhaustiveness check gives a switch.
Complex Switch with JSX
User Role-Based Dashboard
type UserRole = 'admin' | 'moderator' | 'user' | 'guest';
interface DashboardProps {
role: UserRole;
username: string;
}
const Dashboard: React.FC<DashboardProps> = ({ role, username }) => {
const renderDashboard = () => {
switch (role) {
case 'admin':
return (
<div className="dashboard admin-dashboard">
<h1>๐ Admin Dashboard</h1>
<p>Welcome, {username}!</p>
<ul>
<li>๐ฅ Manage Users</li>
<li>โ๏ธ System Settings</li>
<li>๐ Analytics</li>
<li>๐ Security</li>
</ul>
</div>
);
case 'moderator':
return (
<div className="dashboard moderator-dashboard">
<h1>๐ก๏ธ Moderator Dashboard</h1>
<p>Welcome, {username}!</p>
<ul>
<li>๐ Review Content</li>
<li>๐ซ Moderate Posts</li>
<li>๐ฌ User Reports</li>
</ul>
</div>
);
case 'user':
return (
<div className="dashboard user-dashboard">
<h1>๐ค User Dashboard</h1>
<p>Welcome back, {username}!</p>
<ul>
<li>๐ My Posts</li>
<li>โญ Favorites</li>
<li>๐ Notifications</li>
</ul>
</div>
);
case 'guest':
return (
<div className="dashboard guest-dashboard">
<h1>๐ Welcome, Guest!</h1>
<p>Please sign in to access more features.</p>
<button>Sign In</button>
</div>
);
default:
const _exhaustive: never = role;
return <div>Unknown role</div>;
}
};
return renderDashboard();
};
โ When to Use Switch vs Object Mapping
| Pattern | Best For | Example |
|---|---|---|
| Switch Statement | Complex rendering logic, different components per case | Rendering completely different layouts |
| Object Mapping | Same structure with different data/config | Alert types, status badges, icons |
โฉ๏ธ Early Returns
Early returns โ also called guard clauses โ are the antidote to the "pyramid of doom" you get from deeply nested ternaries. The idea is simple: handle every unusual or not-ready case first, returning immediately for each, so that by the time you reach the bottom of the function you are guaranteed to be on the "happy path" with valid data. Think of a bouncer at a club checking IDs one at a time: anyone who fails is turned away at the door, and everyone still inside is known to be good. This pattern pairs beautifully with TypeScript, because each guard narrows the types โ after if (!user) return โฆ, TypeScript knows user is non-null for the rest of the function.
Basic Early Return Pattern
Loading and Error States
interface DataDisplayProps {
data: string[] | null;
isLoading: boolean;
error: string | null;
}
const DataDisplay: React.FC<DataDisplayProps> = ({ data, isLoading, error }) => {
// Early return for loading
if (isLoading) {
return (
<div className="loading">
<p>โณ Loading data...</p>
</div>
);
}
// Early return for error
if (error) {
return (
<div className="error">
<h3>โ Error</h3>
<p>{error}</p>
</div>
);
}
// Early return for empty data
if (!data || data.length === 0) {
return (
<div className="empty">
<p>๐ญ No data available</p>
</div>
);
}
// Main render - we know we have valid data!
return (
<div className="data-display">
<h2>Data List</h2>
<ul>
{data.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
Why Early Returns Are Better
๐ก Compare: Nested vs Early Returns
// โ Nested conditionals (hard to read)
const UserProfile: React.FC<{ user: User | null }> = ({ user }) => {
return (
<div>
{user ? (
user.isPremium ? (
user.hasCompletedProfile ? (
<div>
<h1>{user.name}</h1>
<PremiumBadge />
{/* Complex profile JSX */}
</div>
) : (
<CompleteProfilePrompt />
)
) : (
<UpgradeToPremium />
)
) : (
<SignInPrompt />
)}
</div>
);
};
// โ
Early returns (much cleaner!)
const UserProfile: React.FC<{ user: User | null }> = ({ user }) => {
// Handle each special case first
if (!user) {
return <SignInPrompt />;
}
if (!user.isPremium) {
return <UpgradeToPremium />;
}
if (!user.hasCompletedProfile) {
return <CompleteProfilePrompt />;
}
// Main happy path - no nesting!
return (
<div>
<h1>{user.name}</h1>
<PremiumBadge />
{/* Complex profile JSX */}
</div>
);
};
Real-World Example: Auth Flow
Protected Component
interface ProtectedPageProps {
isAuthenticated: boolean;
isLoading: boolean;
hasPermission: boolean;
user: User | null;
}
const ProtectedPage: React.FC<ProtectedPageProps> = ({
isAuthenticated,
isLoading,
hasPermission,
user
}) => {
// Check loading first
if (isLoading) {
return (
<div className="loading-screen">
<div className="spinner"></div>
<p>Checking credentials...</p>
</div>
);
}
// Check authentication
if (!isAuthenticated) {
return (
<div className="auth-required">
<h2>๐ Authentication Required</h2>
<p>Please log in to view this page.</p>
<button>Log In</button>
</div>
);
}
// Check permissions
if (!hasPermission) {
return (
<div className="no-permission">
<h2>๐ซ Access Denied</h2>
<p>You don't have permission to view this page.</p>
<button>Go Back</button>
</div>
);
}
// Null check (though TypeScript knows it's not null here)
if (!user) {
return <div>Error: User data missing</div>;
}
// Main protected content - we're good to go!
return (
<div className="protected-content">
<h1>Welcome, {user.name}! ๐</h1>
<p>This is premium protected content.</p>
{/* Protected features */}
</div>
);
};
โ Benefits of Early Returns
- Reduces nesting: No deeply nested ternaries or conditionals
- Improves readability: Each condition is clear and isolated
- Easier debugging: You can add console.logs or breakpoints easily
- Better error handling: Edge cases are handled first
- Clean happy path: Main logic isn't cluttered with conditionals
๐ Visual: Nested vs Early Returns Code Structure
Hover over each section to see how the code structure differs:
Early returns keep your main logic clean and visible, not buried under layers of conditions.
๐จ Common Rendering Patterns
Let's explore real-world patterns you'll use constantly in React development.
The Guard-Clause Order That Almost Never Changes
Nearly every data-driven component asks the same four questions, in the same order. Check the "not ready" states first (loading, then error, then empty), and only fall through to the real content once you have proven the data is present and non-empty. That ordering is what lets the final return assume it is working with good data.
๐ฎ Interactive: Which State Renders What?
Pick a state to see the guard clause that catches it and the JSX it returns. This is the exact shape of the UserList component below.
Pick a state above …
๐ Choose a state to preview what the user would see.
Pattern 1: Loading โ Error โ Empty โ Content
The Standard Data Flow
interface User {
id: string;
name: string;
email: string;
}
interface UserListProps {
users: User[];
isLoading: boolean;
error: string | null;
}
const UserList: React.FC<UserListProps> = ({ users, isLoading, error }) => {
// 1. Loading state
if (isLoading) {
return (
<div className="user-list-loading">
<div className="spinner"></div>
<p>Loading users...</p>
</div>
);
}
// 2. Error state
if (error) {
return (
<div className="user-list-error">
<h3>โ Error Loading Users</h3>
<p>{error}</p>
<button>Try Again</button>
</div>
);
}
// 3. Empty state
if (users.length === 0) {
return (
<div className="user-list-empty">
<h3>๐ญ No Users Found</h3>
<p>There are no users to display.</p>
</div>
);
}
// 4. Content (happy path)
return (
<div className="user-list">
<h2>Users ({users.length})</h2>
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong>
<br />
{user.email}
</li>
))}
</ul>
</div>
);
};
Pattern 2: Feature Flags
Toggle Features On/Off
interface FeatureFlags {
showNewDesign: boolean;
enableComments: boolean;
enableSharing: boolean;
}
interface ArticleProps {
title: string;
content: string;
features: FeatureFlags;
}
const Article: React.FC<ArticleProps> = ({ title, content, features }) => (
<article className={features.showNewDesign ? 'new-design' : 'old-design'}>
<h1>{title}</h1>
<div>{content}</div>
{/* Show comments section if enabled */}
{features.enableComments && (
<section className="comments">
<h3>๐ฌ Comments</h3>
<CommentList />
</section>
)}
{/* Show sharing buttons if enabled */}
{features.enableSharing && (
<div className="share-buttons">
<button>Share on Twitter</button>
<button>Share on Facebook</button>
</div>
)}
</article>
);
Pattern 3: Skeleton Screens
Better Than Spinners
interface ProfileCardProps {
user: User | null;
isLoading: boolean;
}
const ProfileCard: React.FC<ProfileCardProps> = ({ user, isLoading }) => {
// Show skeleton while loading
if (isLoading) {
return (
<div className="profile-card skeleton">
<div className="skeleton-avatar"></div>
<div className="skeleton-text skeleton-text-long"></div>
<div className="skeleton-text skeleton-text-short"></div>
</div>
);
}
if (!user) {
return <div>No user data</div>;
}
return (
<div className="profile-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
</div>
);
};
Pattern 4: Optimistic UI
Show Changes Before Confirmation
const LikeButton: React.FC = () => {
const [isLiked, setIsLiked] = useState(false);
const [likeCount, setLikeCount] = useState(42);
const [isPending, setIsPending] = useState(false);
const handleLike = async () => {
// Optimistic update
const previousState = isLiked;
const previousCount = likeCount;
setIsLiked(!isLiked);
setLikeCount(isLiked ? likeCount - 1 : likeCount + 1);
setIsPending(true);
try {
await api.toggleLike();
} catch (error) {
// Revert on error
setIsLiked(previousState);
setLikeCount(previousCount);
} finally {
setIsPending(false);
}
};
return (
<button
onClick={handleLike}
className={isLiked ? 'liked' : 'not-liked'}
disabled={isPending}
>
{isLiked ? 'โค๏ธ' : '๐ค'} {likeCount}
</button>
);
};
Pattern 5: Permission-Based Rendering
Show UI Based on User Permissions
type Permission = 'read' | 'write' | 'delete' | 'admin';
interface DocumentProps {
document: Document;
userPermissions: Permission[];
}
const DocumentView: React.FC<DocumentProps> = ({ document, userPermissions }) => {
const hasPermission = (permission: Permission) =>
userPermissions.includes(permission);
return (
<div className="document">
<h1>{document.title}</h1>
<p>{document.content}</p>
<div className="actions">
{/* Everyone can read */}
<button>๐ View</button>
{/* Only with write permission */}
{hasPermission('write') && (
<button>โ๏ธ Edit</button>
)}
{/* Only with delete permission */}
{hasPermission('delete') && (
<button>๐๏ธ Delete</button>
)}
{/* Only admins */}
{hasPermission('admin') && (
<button>โ๏ธ Settings</button>
)}
</div>
</div>
);
};
๐๏ธ Hands-on Practice
Let's put everything together with a comprehensive exercise!
๐๏ธ Exercise: Build a Status Dashboard
Create a dashboard component that displays different states and information based on various conditions.
Requirements:
- Create a
StatusDashboardcomponent - Show loading state while data fetches
- Show error state if fetch fails
- Show empty state if no data
- Display user info when authenticated
- Show login prompt when not authenticated
- Use role-based rendering (admin/user/guest)
- Include feature flags for optional sections
Starter Code:
interface DashboardData {
user: {
name: string;
role: 'admin' | 'user' | 'guest';
isPremium: boolean;
} | null;
stats: {
posts: number;
followers: number;
following: number;
} | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
features: {
showAnalytics: boolean;
showNotifications: boolean;
};
}
const StatusDashboard: React.FC<DashboardData> = (props) => {
// Your code here!
return (
<div className="dashboard">
{/* Implement conditional rendering */}
</div>
);
};
// Test the component
const App = () => {
const [dashboardData, setDashboardData] = useState<DashboardData>({
user: null,
stats: null,
isAuthenticated: false,
isLoading: true,
error: null,
features: {
showAnalytics: true,
showNotifications: true
}
});
return <StatusDashboard {...dashboardData} />;
};
๐ก Hint 1: Structure
Start with early returns for loading and error states:
if (isLoading) return <LoadingView />;
if (error) return <ErrorView error={error} />;
if (!isAuthenticated) return <LoginPrompt />;
๐ก Hint 2: Role-Based Content
Use switch or object mapping for role-based rendering:
const roleConfig = {
admin: { icon: '๐', title: 'Admin Dashboard' },
user: { icon: '๐ค', title: 'User Dashboard' },
guest: { icon: '๐', title: 'Guest Dashboard' }
};
const config = roleConfig[user.role];
โ Solution
const StatusDashboard: React.FC<DashboardData> = ({
user,
stats,
isAuthenticated,
isLoading,
error,
features
}) => {
// Early return: Loading
if (isLoading) {
return (
<div className="dashboard loading">
<div className="spinner"></div>
<p>Loading dashboard...</p>
</div>
);
}
// Early return: Error
if (error) {
return (
<div className="dashboard error">
<h2>โ Error</h2>
<p>{error}</p>
<button>Retry</button>
</div>
);
}
// Early return: Not authenticated
if (!isAuthenticated || !user) {
return (
<div className="dashboard login-prompt">
<h2>๐ Login Required</h2>
<p>Please log in to view your dashboard.</p>
<button>Log In</button>
</div>
);
}
// Role configuration
const roleConfig = {
admin: {
icon: '๐',
title: 'Admin Dashboard',
permissions: ['view-all', 'edit-all', 'delete']
},
user: {
icon: '๐ค',
title: 'User Dashboard',
permissions: ['view-own', 'edit-own']
},
guest: {
icon: '๐',
title: 'Guest Dashboard',
permissions: ['view-limited']
}
};
const config = roleConfig[user.role];
// Main dashboard render
return (
<div className={`dashboard dashboard-${user.role}`}>
<header>
<h1>{config.icon} {config.title}</h1>
<p>Welcome back, {user.name}!</p>
{user.isPremium && <span className="badge">โญ Premium</span>}
</header>
{/* Show stats if available */}
{stats && (
<section className="stats">
<div className="stat-card">
<h3>{stats.posts}</h3>
<p>Posts</p>
</div>
<div className="stat-card">
<h3>{stats.followers}</h3>
<p>Followers</p>
</div>
<div className="stat-card">
<h3>{stats.following}</h3>
<p>Following</p>
</div>
</section>
)}
{/* Analytics (feature flag) */}
{features.showAnalytics && (
<section className="analytics">
<h2>๐ Analytics</h2>
<p>Your content performance...</p>
</section>
)}
{/* Notifications (feature flag) */}
{features.showNotifications && (
<section className="notifications">
<h2>๐ Notifications</h2>
<p>You have new updates...</p>
</section>
)}
{/* Admin-only section */}
{user.role === 'admin' && (
<section className="admin-tools">
<h2>๐ ๏ธ Admin Tools</h2>
<button>Manage Users</button>
<button>System Settings</button>
</section>
)}
{/* Premium-only features */}
{user.isPremium ? (
<section className="premium-features">
<h2>โญ Premium Features</h2>
<p>Exclusive content for premium members...</p>
</section>
) : (
<section className="upgrade-prompt">
<h2>โญ Upgrade to Premium</h2>
<p>Unlock exclusive features!</p>
<button>Upgrade Now</button>
</section>
)}
</div>
);
};
๐๏ธ Challenge Exercise: Weather App
Build a weather display component with these states:
- Loading weather data
- Location permission denied
- API error
- Different displays for sunny/rainy/cloudy/snowy
- Temperature units toggle (Celsius/Fahrenheit)
- Show/hide detailed forecast based on user preference
Bonus: Use all the conditional rendering patterns we learned!
โจ Best Practices
โ Do's
- Use early returns for edge cases and error states
- Keep conditions simple - extract complex logic into functions
- Use explicit comparisons (
> 0) instead of truthy checks when appropriate - Prefer
??over||when 0, false, or '' are valid values - Use object mapping instead of switch for configuration-based rendering
- Extract conditional logic into well-named functions for readability
- Handle all states: loading, error, empty, and success
- Use TypeScript to ensure exhaustive checks with union types
โ Don'ts
- Don't nest ternaries more than 2 levels deep - use early returns instead
- Don't use && with numbers or counts without explicit comparison
- Don't forget null/undefined checks before accessing properties
- Don't render "true" or "false" accidentally - wrap booleans properly
- Don't over-complicate - sometimes a simple if/else is best
- Don't ignore TypeScript warnings about potential nulls
- Don't render different component types in the same condition chain (breaks React's reconciliation)
Readability Guidelines
// โ Hard to read
const Component = ({ a, b, c }) => (
<div>
{a && b ? c ? <X /> : <Y /> : a || b ? <Z /> : <W />}
</div>
);
// โ
Clear and maintainable
const Component = ({ a, b, c }) => {
if (!a && !b) return <W />;
if (a || b) {
if (!a || !b) return <Z />;
return c ? <X /> : <Y />;
}
};
// โ
Even better with named helpers
const Component = ({ a, b, c }) => {
const hasBasicRequirements = a && b;
const hasAdvancedFeatures = hasBasicRequirements && c;
if (!a && !b) return <W />;
if (!hasBasicRequirements) return <Z />;
return hasAdvancedFeatures ? <X /> : <Y />;
};
Performance Considerations
๐ก Conditional Rendering and Re-renders
// โ Creates new functions on every render
const ExpensiveComponent = ({ showDetails }) => (
<div>
{showDetails && heavyComputation()} {/* Runs every render! */}
</div>
);
// โ
Compute conditionally
const ExpensiveComponent = ({ showDetails }) => {
const details = showDetails ? heavyComputation() : null;
return (
<div>
{details && <div>{details}</div>}
</div>
);
};
// โ
Or use early return
const ExpensiveComponent = ({ showDetails }) => {
if (!showDetails) {
return <div>Basic view</div>;
}
const details = heavyComputation();
return <div>{details}</div>;
};
TypeScript Exhaustiveness Checking
Ensure All Cases Are Handled
type Status = 'idle' | 'loading' | 'success' | 'error';
const StatusDisplay: React.FC<{ status: Status }> = ({ status }) => {
switch (status) {
case 'idle':
return <div>Ready</div>;
case 'loading':
return <div>Loading...</div>;
case 'success':
return <div>Success!</div>;
case 'error':
return <div>Error!</div>;
default:
// TypeScript will error if we're missing a case!
const _exhaustive: never = status;
return <div>Unknown status</div>;
}
};
๐ Summary
What You Learned
Congratulations! You now know all the essential conditional rendering patterns in React:
| Pattern | Syntax | Best For |
|---|---|---|
| If/Else | if (condition) return <A />; else return <B />; |
Simple binary conditions outside JSX |
| Ternary | {condition ? <A /> : <B />} |
Inline either/or rendering in JSX |
| && Operator | {condition && <Component />} |
Show/hide single elements |
| || Operator | {value || 'default'} |
Fallback values |
| ?? Operator | {value ?? 'default'} |
Fallback when null/undefined |
| Switch | switch(value) { case 'a': ... } |
Multiple distinct conditions |
| Early Return | if (error) return <Error />; |
Edge cases and guard clauses |
| Object Map | config[type] |
Configuration-based rendering |
๐ฏ Key Takeaways
- Choose the right tool: Each pattern has its best use case
- Readability matters: Use early returns to avoid deep nesting
- Handle all states: Loading, error, empty, and success
- Be explicit: Use
> 0instead of truthy checks for numbers - Use TypeScript: Exhaustive checking prevents missing cases
- Think about UX: Every state should have appropriate UI feedback
Common Conditional Patterns Cheatsheet
// Loading state
if (isLoading) return <Spinner />;
// Error state
if (error) return <ErrorMessage error={error} />;
// Empty state
if (items.length === 0) return <EmptyState />;
// Authenticated vs Unauthenticated
if (!isAuthenticated) return <LoginPrompt />;
// Permissions
if (!hasPermission) return <AccessDenied />;
// Feature flags
{featureEnabled && <NewFeature />}
// Null checks
{user?.profile && <ProfileCard profile={user.profile} />}
// Fallback values
<p>{username ?? 'Anonymous'}</p>
// Multiple conditions
{isVisible && !isDisabled && <Button />}
// Either/or
{isPremium ? <PremiumFeatures /> : <UpgradePrompt />}
Practice Checklist
Make sure you can:
- โ Use if/else statements for early returns
- โ Write inline ternaries in JSX
- โ Use && operator without falsy value bugs
- โ Choose between || and ?? appropriately
- โ Implement switch statements or object mapping
- โ Handle loading, error, and empty states
- โ Write clean, readable conditional code
- โ Avoid common conditional rendering pitfalls
๐ What's Next?
In the next lesson, we'll tackle the Module 3 Project, where you'll build a fully functional Todo Application that combines everything you've learned:
- useState for managing todo state
- Forms for adding new todos
- Lists for displaying todos
- Conditional rendering for loading, empty, and error states
- Filter controls (All, Active, Completed)
Get ready to put all your skills together! ๐ช