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.

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>
  )
}

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.

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>
  )
}

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

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>
  )
}

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.

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>
  )
}

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

Early Return

Since the only JSX needed is the <Layout> call, we can move the conditional checks out of JSX and return early.

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>
  )
}

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 <Layout> in each branch is acceptable because it isolates subtle differences and keeps the component flexible. Adding a title prop demonstrates this:

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>
  )
}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.