Conditional Rendering — Patterns for Showing and Hiding UI

Conditional rendering is how React shows different UI based on state or data. Unlike templates in other frameworks that have dedicated v-if or *ngIf directives, React uses plain JavaScript — ternary operators, short-circuit evaluation, and early returns. Knowing which pattern to use in each situation makes React components readable and maintainable. The blog application uses conditional rendering constantly: showing a spinner while loading, an error message if the API fails, and the actual posts when data is ready.

Conditional Rendering Patterns

// ── 1. Ternary — for if/else with two outcomes ─────────────────────────────────
function UserGreeting({ user }) {
    return (
        <div>
            {user ? (
                <span>Welcome back, {user.name}!</span>
            ) : (
                <a href="/login">Please log in</a>
            )}
        </div>
    );
}

// ── 2. && short-circuit — for "show only if true" ─────────────────────────────
function PostCard({ post, isOwner }) {
    return (
        <article>
            <h2>{post.title}</h2>
            {/* Show edit button only if user owns the post */}
            {isOwner && (
                <a href={`/posts/${post.id}/edit`} className="text-blue-500">Edit</a>
            )}
            {/* ⚠ Pitfall: post.tags.length && renders "0" when empty */}
            {post.tags.length > 0 && (
                <TagList tags={post.tags} />
            )}
        </article>
    );
}

// ── 3. Early return — for guard clauses ───────────────────────────────────────
function PostDetail({ post }) {
    if (!post) {
        return <p className="text-gray-400">Post not found.</p>;
    }
    if (post.status === "draft" && !isAdmin) {
        return <p className="text-yellow-600">This post is a draft.</p>;
    }
    // Main render — only reached if all guards pass
    return (
        <article>
            <h1>{post.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.body }} />
        </article>
    );
}
Note: dangerouslySetInnerHTML renders raw HTML — use it only with content you fully trust (your own server-generated HTML) or with a sanitisation library like DOMPurify. Blog post bodies from the API should pass through DOMPurify before rendering to prevent XSS attacks: const clean = DOMPurify.sanitize(post.body); return <div dangerouslySetInnerHTML={{ __html: clean }} />. The “dangerous” in the name is intentional — it is a signal to review every usage carefully.
Tip: The tri-state loading/error/data pattern handles the three states of any async operation cleanly. Instead of checking !data (which is true for both loading and error states), track all three explicitly with separate boolean/object flags: if (isLoading) return <Skeleton />, if (error) return <ErrorMessage error={error} />, then render the data. This pattern maps directly to how TanStack Query (covered in Chapter 41) reports fetch state.
Warning: Deeply nested conditional rendering creates “pyramid of doom” code that is hard to follow. If you find yourself with three or more levels of nested ternary operators, extract parts into separate components or helper functions. A rule of thumb: if the ternary expression is longer than one line, consider extracting it into a named function or component that returns JSX — the name itself documents what is being conditionally rendered.

The Loading / Error / Data Tri-State Pattern

// The three states of any data fetch:
// 1. isLoading — show a skeleton or spinner
// 2. error — show an error message
// 3. data — show the actual content

function PostFeed() {
    const [posts,     setPosts]     = React.useState([]);
    const [isLoading, setIsLoading] = React.useState(true);
    const [error,     setError]     = React.useState(null);

    // (Data fetching with useEffect — covered in Chapter 36)

    // ── Tri-state render logic ─────────────────────────────────────────────────

    // State 1: Loading
    if (isLoading) {
        return (
            <div className="grid gap-4">
                {[1, 2, 3].map((i) => (
                    <PostCardSkeleton key={i} />   // loading placeholder
                ))}
            </div>
        );
    }

    // State 2: Error
    if (error) {
        return (
            <div className="text-center py-12">
                <p className="text-red-500 mb-4">{error.message}</p>
                <button onClick={retry} className="text-blue-500 hover:underline">
                    Retry
                </button>
            </div>
        );
    }

    // State 3: Empty data (a sub-state of "data available")
    if (posts.length === 0) {
        return (
            <div className="text-center py-12 text-gray-400">
                <p>No posts yet. <a href="/posts/new" className="text-blue-500">Write one?</a></p>
            </div>
        );
    }

    // State 4: Has data
    return (
        <div className="grid gap-4">
            {posts.map((post) => (
                <PostCard key={post.id} post={post} />
            ))}
        </div>
    );
}

Switch Statement for Multi-Case Rendering

// ── Render different components based on a status value ───────────────────────
function PostStatus({ status }) {
    switch (status) {
        case "published":
            return <Badge variant="published">Published</Badge>;
        case "draft":
            return <Badge variant="draft">Draft</Badge>;
        case "archived":
            return <Badge variant="default">Archived</Badge>;
        default:
            return <Badge>{status}</Badge>;
    }
}

// ── Object lookup — cleaner than switch for simple cases ─────────────────────
const STATUS_LABELS = {
    published: <Badge variant="published">Published</Badge>,
    draft:     <Badge variant="draft">Draft</Badge>,
    archived:  <Badge variant="default">Archived</Badge>,
};

function PostStatus({ status }) {
    return STATUS_LABELS[status] ?? <Badge>{status}</Badge>;
}

Common Mistakes

Mistake 1 — The 0 rendering bug

❌ Wrong — renders “0” when count is 0:

{post.comment_count && <CommentBadge count={post.comment_count} />}

✅ Correct:

{post.comment_count > 0 && <CommentBadge count={post.comment_count} />}   // ✓

Mistake 2 — Nested ternary (unreadable):

❌ Wrong — three levels deep:

{isLoading ? <Spinner /> : error ? <Error msg={error} /> : posts.length === 0 ? <Empty /> : <List />}

✅ Correct — use early returns for clarity.

Quick Reference

Pattern Use When Code
Ternary Two outcomes {condition ? <A /> : <B />}
&& guard Show or nothing {condition && <A />}
Early return Guard clauses if (!data) return <Empty />
Switch Multiple cases switch(status) { case: ... }
Object lookup Simple enum mapping MAP[key] ?? <Default />

🧠 Test Yourself

You write {posts.length && <PostList posts={posts} />}. What renders when posts is an empty array?