# Avoid Waterfalls in React Suspense

Used: react@19.0.0 and react-router@7.0.0

React’s Suspense is great for orchestrating asynchronous UI rendering, but it’s easy to accidentally introduce a “waterfall”, where one piece of async work waits unnecessarily for another to finish before starting.

In this tutorial, we’ll explore how **sibling** and **nested** Suspense boundaries behave, how this relates to JavaScript’s async patterns, and how to structure your data fetching to avoid delays.

## Suspense Boundaries and Promise Patterns

Think of Suspense boundaries as async functions. The difference between **sibling** and **nested** boundaries maps closely to how you write your `await` statements.

### Nested Suspense Boundaries → Sequential `await`

When you nest Suspense boundaries, the inner boundary won’t start loading until the outer one finishes rendering. This is like writing:

```ts
let d1 = await getData1();
let d2 = await getData2();
```

Here, `getData2()` doesn’t even start until `getData1()` has resolved. That’s the waterfall problem.

### Sibling Suspense Boundaries → `Promise.all`

If you place two Suspense boundaries as siblings, they can load in parallel, just like:

```ts
let [d1, d2] = await Promise.all([getData1(), getData2()]);
```

Both promises start immediately. React can even render the `d1` UI while still waiting for `d2`.

### Nested Boundaries Without the Waterfall

Sometimes your UI is **nested** but your **data is independent**. You want nested boundaries for layout, but without sequential delays. In that case, you can “hide” the waterfall by starting the second request early:

```ts
let p2 = getData2();
let d1 = await getData1();
let d2 = await p2;
```

Now, `getData2()` starts immediately, even though you `await` it later.

## Applying This in React Server Components (RSC)

### Example Using `React.cache`

```tsx
const getData2Cached = React.cache(getData2);

export default async function Page() {
  // Start data2 early
  getData2Cached();

  let d1 = await getData1();
  return (
    <>
      <UI1 data={d1} />
      <Suspense fallback={<LoadingD2 />}>
        <Child />
      </Suspense>
    </>
  );
}

async function Child() {
  let d2 = await getData2Cached();
  return <UI2 data={d2} />;
}
```

### Example Passing a Promise to a Client Component

You can also start the second request in a server component and pass the **promise** down to a client component. In the client component, you can use `React.use` to unwrap it.

```tsx
// Server Component
import ClientComponent from "./client-component";

export default async function Page() {
  let p2 = getData2();
  let d1 = await getData1();
  return (
    <>
      <UI1 data={d1} />
      <Suspense fallback={<LoadingD2 />}>
        <ClientComponent promise={p2} />
      </Suspense>
    </>
  );
}
```

```tsx
// Client Component
"use client";
import { use } from "react";

interface Props {
  promise: ReturnType<typeof getData2>;
}

export default function ClientComponent({ promise }: Props) {
  let d2 = use(promise);
  return <UI2 data={d2} />;
}
```

## Using React Router Deferred Promises

React Router lets you start independent data fetching in parallel, even when some data is needed earlier.

```tsx
import type { Route } from "./+types";
import { Suspense } from "react";

export async function loader() {
  let p2 = getData2();
  let d1 = await getData1();
  return { d1, p2 };
}

export default function Component({ loaderData }: Route.ComponentProps) {
  return (
    <>
      <UI1 data1={loaderData.d1} />
      <Suspense fallback={<LoadingD2 />}>
        {loaderData.p2.then((d2) => (
          <UI2 data2={d2} />
        ))}
      </Suspense>
    </>
  );
}
```

With this approach, `getData2()` starts in the loader before `getData1()` finishes, avoiding the waterfall while still rendering the parts of the UI that depend only on `d1` first.
