Skip to content

React · Components

React Conditional Rendering — A Complete Interview Guide

7 min read Updated 2026-06-23 Share:

Practice Conditional Rendering interview questions

The four patterns every React developer needs

Conditional rendering in React is a topic interviewers love because it reveals whether you write clean, readable code or whether you nest ternaries until the intent is buried. There are four main patterns, each with the right use case.

1. Logical &&

The cleanest option when you have one optional element and no fallback:

{isLoggedIn && <UserMenu />}
{hasError && <ErrorBanner message={error} />}

When the left side is true, the right side renders. When false, nothing.

2. Ternary

Exactly two alternatives:

{isLoading ? <Spinner /> : <Content data={data} />}
{user ? <Avatar user={user} /> : <GuestBadge />}

Use null for the false branch when you want nothing: {isAdmin ? <AdminPanel /> : null} is equivalent to {isAdmin && <AdminPanel />}.

3. Variable / if-else before the return

For three or more outcomes, or when the condition logic is complex:

function Page({ status, data, error }) {
  let content
  if (status === 'loading') content = <Spinner />
  else if (status === 'error') content = <ErrorMessage error={error} />
  else if (!data) content = <Empty />
  else content = <DataView data={data} />

  return <main>{content}</main>
}

4. Early return (guard clause)

For components that have nothing to show in certain states:

function Profile({ user }) {
  if (!user) return null
  if (user.isBanned) return <BannedMessage />
  return <ProfileContent user={user} />
}

Guards at the top are especially clean because they separate the "edge cases" from the "happy path" — the main render is always the last thing in the function.

The falsy zero bug — the most common && mistake

&& returns the left operand when it's falsy. If that operand is the number 0, React renders 0 — a valid renderable value.

const messages = []   // length is 0

// ❌ Renders "0" when messages is empty
return <div>{messages.length && <MessageList />}</div>

// ✅ Force to a boolean
return <div>{messages.length > 0 && <MessageList />}</div>
return <div>{Boolean(messages.length) && <MessageList />}</div>
return <div>{!!messages.length && <MessageList />}</div>

The same trap exists for any number that could be 0: count, items.length, score, unreadCount. Never use a number directly with &&; always compare.

Returning null

Returning null from a component renders nothing but keeps the component mounted. Effects run and refs attach even when the output is invisible.

function Notification({ show, message }) {
  if (!show) return null
  return <div className="notification">{message}</div>
}

This is different from not rendering the component at all:

  • return null → mounted, effects run, state persists.
  • {show && <Notification />} → when show is false, the component is unmounted: state resets, effects clean up.

Use null when you need the component in the tree (for subscriptions or imperative refs) but want to hide its output. Use conditional rendering when you want the component fully gone.

CSS display:none vs conditional rendering

CSS display:noneConditional rendering
DOM presenceYesNo
State preservedYesNo (reset on unmount)
Effects runningYesNo (clean up on unmount)
PerformanceHeavier mountCheaper when hidden
// CSS: always mounted, always running effects
<div style={{ display: isOpen ? 'block' : 'none' }}>
  <HeavyWidget />
</div>

// Conditional: unmounts when isOpen is false
{isOpen && <HeavyWidget />}

Default to conditional rendering. Use CSS visibility only when:

  • Remounting is prohibitively expensive (e.g. a complex editor widget).
  • You must preserve internal state across hides (e.g. a video that should keep its playback position).

The guard-clause pattern for loading/error states

A robust pattern for async data is to guard each sad state at the top and keep the happy path at the bottom:

function UserProfile({ userId }) {
  const { data: user, loading, error } = useUser(userId)

  if (loading) return <Skeleton />
  if (error)   return <ErrorMessage error={error} />
  if (!user)   return null

  // Happy path — no nesting
  return (
    <article>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </article>
  )
}

The order matters: check loading first (before you have data), error second, then empty/null, then render the data. Checking if (!user) before if (loading) would crash because user is undefined during loading.

Avoid deeply nested ternaries

Nested ternaries are a code smell. If you find yourself writing:

{loading ? <Spinner /> : error ? <Error /> : data ? <View /> : <Empty />}

Stop. Refactor to a variable or a helper:

// ✅ Variable — clear, flat, readable
let body
if (loading) body = <Spinner />
else if (error) body = <Error message={error} />
else if (!data) body = <Empty />
else body = <DataView data={data} />

return <main>{body}</main>

Or extract a component whose only job is this switching logic. The rule of thumb: never nest ternaries more than one level deep.

Conditional class names

// Simple ternary
<button className={isActive ? 'btn active' : 'btn'}>…</button>

// Template literal for base + modifiers
<div className={`card ${isHighlighted ? 'card--highlighted' : ''} ${hasError ? 'card--error' : ''}`}>

// clsx library — cleaner for multiple conditions
import clsx from 'clsx'
<div className={clsx('card', { 'card--highlighted': isHighlighted, 'card--error': hasError })}>

For more than one conditional class, clsx (or classnames) pays for itself immediately — it handles undefined and false values cleanly.

Permission-based rendering

Keep auth logic out of individual components. A dedicated gate component or hook centralizes it:

// Gate component
function Can({ role, children }) {
  const { user } = useAuth()
  return user?.role === role ? children : null
}

<Can role="admin">
  <DeleteButton />
</Can>

// Hook
function usePermission(role) {
  const { user } = useAuth()
  return user?.role === role
}

const canDelete = usePermission('admin')
{canDelete && <DeleteButton />}

lazy + Suspense as conditional rendering

React.lazy combined with Suspense is a built-in conditional-rendering pattern for code that loads asynchronously:

const AdminPanel = lazy(() => import('./AdminPanel'))

function App({ user }) {
  return (
    <Suspense fallback={<Spinner />}>
      {user.isAdmin && <AdminPanel />}
    </Suspense>
  )
}

When <AdminPanel /> first renders, React suspends, shows <Spinner />, loads the bundle, and retries. Your && condition still controls whether it renders at all — Suspense handles the loading-state fallback for the async import.

What interviewers are testing

Conditional rendering questions are checking whether you:

  1. Know all four patterns and can pick the right one.
  2. Understand the falsy zero bug with &&.
  3. Know the difference between return null and not mounting.
  4. Can keep complex conditions readable (no nested ternaries).
  5. Understand display:none vs unmounting trade-offs.

The cleanliness of your conditional rendering code signals how maintainable your components will be in a real codebase.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel