Skip to main content

๐Ÿ”€ 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

graph TD A["๐Ÿ”€ Conditional Rendering"] --> B["๐Ÿ“ if/else statements"] A --> C["โ“ Ternary operator ?:"] A --> D["โž• Logical && operator"] A --> E["๐Ÿ”„ Logical || operator"] A --> F["๐Ÿ”€ Switch statements"] A --> G["โ†ฉ๏ธ Early returns"] A --> H["๐Ÿ“ฆ Element variables"] B --> B1["Best for: Complex logic"] C --> C1["Best for: Inline either/or"] D --> D1["Best for: Show/hide"] E --> E1["Best for: Fallback values"] F --> F1["Best for: Multiple cases"] G --> G1["Best for: Guard clauses"] H --> H1["Best for: Pre-calculated JSX"] style A fill:#667eea,stroke:#5a67d8,color:#fff style B fill:#e3f2fd,stroke:#2196F3 style C fill:#e3f2fd,stroke:#2196F3 style D fill:#e3f2fd,stroke:#2196F3 style E fill:#e3f2fd,stroke:#2196F3 style F fill:#e3f2fd,stroke:#2196F3 style G fill:#e3f2fd,stroke:#2196F3 style H fill:#e3f2fd,stroke:#2196F3 style B1 fill:#e8f5e9,stroke:#4CAF50 style C1 fill:#e8f5e9,stroke:#4CAF50 style D1 fill:#e8f5e9,stroke:#4CAF50 style E1 fill:#e8f5e9,stroke:#4CAF50 style F1 fill:#e8f5e9,stroke:#4CAF50 style G1 fill:#e8f5e9,stroke:#4CAF50 style H1 fill:#e8f5e9,stroke:#4CAF50

๐ŸŽฏ Interactive: Which Pattern Should You Use?

Click through this decision tree to find the best conditional rendering pattern for your use case:

What do you need? Either/Or Choice Show/Hide One Thing Multiple Cases Default Value Inline JSX? Complex logic? โœ… Ternary condition ? a : b โœ… If/Else Early returns โœ… && Operator condition && jsx โœ… Switch/Map config[type] โœ… || or ?? value ?? default Click a node above to explore! Start by clicking "What do you need?"

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:

isLoggedIn ? โŒฉDashboard/โŒช : โŒฉLogin/โŒช true โ†’ Returns first option โŒฉDashboard /โŒช
Rendered Output:

โž• 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:

โŒ Nested Conditionals โœ… Early Returns return ( {user ? ( user.isPremium ? ( user.hasProfile ? ( ๐Ÿ˜ต Main Content (buried deep!) ) : <ProfilePrompt /> ) : <UpgradePrompt /> ) : <LoginPrompt />} ); 4 levels deep ๐Ÿ˜ฐ if (!user) return <LoginPrompt />; Guard โ‘  if (!user.isPremium) return <UpgradePrompt />; Guard โ‘ก if (!user.hasProfile) return <ProfilePrompt />; Guard โ‘ข // Happy path - no nesting! return ( <div> <h1>{user.name}</h1> </div> ๐Ÿ˜Š Main Content All at the same level! ๐ŸŽ‰

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.

graph TD S["Component renders"] --> Q1{"isLoading?"} Q1 -->|"yes"| L["return <Spinner />"] Q1 -->|"no"| Q2{"error?"} Q2 -->|"yes"| E["return <ErrorView />"] Q2 -->|"no"| Q3{"list is empty?"} Q3 -->|"yes"| M["return <EmptyState />"] Q3 -->|"no"| C["return <DataList />"] style L fill:#2196F3,stroke:#1565C0,color:#fff style E fill:#f44336,stroke:#c62828,color:#fff style M fill:#FF9800,stroke:#E65100,color:#fff style C fill:#4CAF50,stroke:#2E7D32,color:#fff
Figure: the standard loading โ†’ error โ†’ empty โ†’ content guard order. Each answered question narrows what can still be on screen.

๐ŸŽฎ 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:

  1. Create a StatusDashboard component
  2. Show loading state while data fetches
  3. Show error state if fetch fails
  4. Show empty state if no data
  5. Display user info when authenticated
  6. Show login prompt when not authenticated
  7. Use role-based rendering (admin/user/guest)
  8. 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 > 0 instead 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! ๐Ÿ’ช