Frontend Development 11 min read

Why Early Returns Make React Component Composition Cleaner

The article explains how treating UI as composable components, avoiding tangled conditional rendering, and using early returns can reduce cognitive load, improve type inference, and make React code easier to extend and maintain.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Why Early Returns Make React Component Composition Cleaner

This article, translated from "Component Composition is great btw," discusses why component composition is the most powerful feature of React and how to handle conditional rendering effectively.

When first learning React, developers focus on the virtual DOM, one‑way data flow, and JSX, but the real advantage lies in composing components into larger ones. Over time, developers realize that grouping related logic, style, and markup together improves code cohesion.

However, many applications fall into the trap of conditional rendering, which can make components hard to evolve.

Conditional Rendering

In JSX you can render components conditionally. The following example renders a shopping list and optionally shows user information.

<code>export function ShoppingList(props: { content: ShoppingList; assignee?: User }) {
  return (
    <Card>
      <CardHeading>Welcome đź‘‹</CardHeading>
      <CardContent>
        {props.assignee ? <UserInfo {...props.assignee} /> : null}
        {props.content.map((item) => (
          <ShoppingItem key={item.id} {...item} />
        ))}
      </CardContent>
    </Card>
  )
}</code>

If no assignee is provided, the user‑info part is omitted.

Conditionally Rendering Multiple States

When the component becomes self‑contained by fetching its own data, more states appear (loading, empty, error). The diff below adds a loading skeleton and handles the empty state.

<code>export function ShoppingList() {
  const { data, isPending } = useQuery(/* ... */)

  return (
    <Card>
      <CardHeading>Welcome đź‘‹</CardHeading>
      <CardContent>
        {data?.assignee ? <UserInfo {...data.assignee} /> : null}
        {isPending ? <Skeleton /> : null}
        {data ? (
          data.content.map((item) => (
            <ShoppingItem key={item.id} {...item} />
          ))
        ) : (
          <EmptyScreen />
        )}
      </CardContent>
    </Card>
  )
}</code>

This introduces a bug: also appears during the loading state. Adding another condition fixes it.

<code>export function ShoppingList() {
  const { data, isPending } = useQuery(/* ... */)

  return (
    <Card>
      <CardHeading>Welcome đź‘‹</CardHeading>
      <CardContent>
        {data?.assignee ? <UserInfo {...data.assignee} /> : null}
        {isPending ? <Skeleton /> : null}
        {!data && !isPending ? <EmptyScreen /> : null}
        {data && data.content.map((item) => (
          <ShoppingItem key={item.id} {...item} />
        ))}
      </CardContent>
    </Card>
  )
}</code>

Even with these fixes, the JSX becomes cluttered with many ternary expressions, increasing cognitive load.

Back to the Drawing Board

Following the React docs, the UI should be broken into visual boxes. The diagram below illustrates shared layout (red) versus state‑specific content (blue).

Layout Duplication Issue

Extract the shared layout into a

Layout

component that accepts children.

<code>function Layout(props: { children: ReactNode }) {
  return (
    <Card>
      <CardHeading>Welcome đź‘‹</CardHeading>
      <CardContent>{props.children}</CardContent>
    </Card>
  )
}

export function ShoppingList() {
  const { data, isPending } = useQuery(/* ... */)

  return (
    <Layout>
      {data?.assignee ? <UserInfo {...data.assignee} /> : null}
      {isPending ? <Skeleton /> : null}
      {!data && !isPending ? <EmptyScreen /> : null}
      {data && data.content.map((item) => (
        <ShoppingItem key={item.id} {...item} />
      ))}
    </Layout>
  )
}</code>

Although this removes some duplication, the conditional logic is still tangled.

Early Return

Since the only JSX needed is the

&lt;Layout&gt;

call, we can move the conditional checks out of JSX and return early.

<code>function Layout(props: { children: ReactNode }) {
  return (
    <Card>
      <CardHeading>Welcome đź‘‹</CardHeading>
      <CardContent>{props.children}</CardContent>
    </Card>
  )
}

export function ShoppingList() {
  const { data, isPending } = useQuery(/* ... */)

  if (isPending) {
    return (
      <Layout>
        <Skeleton />
      </Layout>
    )
  }

  if (!data) {
    return (
      <Layout>
        <EmptyScreen />
      </Layout>
    )
  }

  return (
    <Layout>
      {data.assignee ? <UserInfo {...data.assignee} /> : null}
      {data.content.map((item) => (
        <ShoppingItem key={item.id} {...item} />
      ))}
    </Layout>
  )
}</code>

Early returns provide three main benefits:

Reduced cognitive load: each

if

branch clearly represents a UI state, making the flow top‑to‑bottom like async/await.

Easy extensibility: new states (e.g., error handling) can be added with additional

if

blocks without affecting existing ones.

Better type inference: after handling the

!data

case, TypeScript knows

data

is defined, improving autocomplete and safety.

Layout Repetition Problem

Repeating

&lt;Layout&gt;

in each branch is acceptable because it isolates subtle differences and keeps the component flexible. Adding a

title

prop demonstrates this:

<code>function Layout(props: { children: ReactNode; title?: string }) {
  return (
    <Card>
      <CardHeading>Welcome đź‘‹ {props.title}</CardHeading>
      <CardContent>{props.children}</CardContent>
    </Card>
  )
}

export function ShoppingList() {
  const { data, isPending } = useQuery(/* ... */)

  if (isPending) {
    return (
      <Layout>
        <Skeleton />
      </Layout>
    )
  }

  if (!data) {
    return (
      <Layout>
        <EmptyScreen />
      </Layout>
    )
  }

  return (
    <Layout title={data.title}>
      {data.assignee ? <UserInfo {...data.assignee} /> : null}
      {data.content.map((item) => (
        <ShoppingItem key={item.id} {...item} />
      ))}
    </Layout>
  )
}</code>

Adding more conditions to the layout may indicate a flawed abstraction, suggesting a redesign.

Overall, the piece emphasizes that early returns, combined with thoughtful component composition, help avoid mutually exclusive conditional rendering and keep UI code maintainable.

frontendReactComponent Compositionconditional-renderingEarly Return
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.