Promise.all in the Next.js App Router

Welcome to App Router World — everything is nice and new and shiny here.

This isn't one of those recipe websites so I'll skip the preable and start with the valuable insight:

If you find yourself reaching for Promise.all in RSCs, you probably want Suspense instead.

If you already understand that, skip the rest of the post. Otherwise, here's how I ran into this issue and came up with this rule of thumb.


I was recently working on a Next.js app that needed to fetch from a bunch of data sources. React Server Components make this easy.

In the world of getServerSideProps, I would have written something like this:

export async function getServerSideProps() {
  const [data1, data2, data3] = await Promise.all([
    fetch('https://api1.com'),
    fetch('https://api2.com'),
    fetch('https://api3.com'),
  ])

  return {
    props: {
      data1,
      data2,
      data3,
    },
  }
}

Promise.all makes all my API calls in parallel and returns an array of the results. Works pretty well as long as none of my API calls are slow and works even better if I refactor it to use Incremental Static Regeneration (ISR).

In RSC land, I don't have getServerSideProps. Instead, I can just fetch in my component:

export default async function MyPage() {
  const [data1, data2, data3] = await Promise.all([
    fetch('https://api1.com'),
    fetch('https://api2.com'),
    fetch('https://api3.com'),
  ])

  return (
    <div>
      {data1 && <div>{data1}</div>}
      {data2 && <div>{data2}</div>}
      {data3 && <div>{data3}</div>}
    </div>
  )
}

Pretty cool right? But what happens if one of my API calls is slow? Well, the whole component will be slow to render.

If you've been following Next.js closely, you might ask:

But I thought Next.js App Router implemented streaming? Shouldn't this be fine?

And you'd be right, if we had remembered to use Suspense instead of Promise.all.

import { Suspense } from 'react';

async function Data1() {
  const res = await fetch('https://api1.com');
  const data = await res.json();
  return <div>{data}</div>;
}

async function Data2() {
  const res = await fetch('https://api2.com');
  const data = await res.json();
  return <div>{data}</div>;
}

async function Data3() {
  const res = await fetch('https://api3.com');
  const data = await res.json();
  return <div>{data}</div>;
}

export default function MyPage() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Data1 />
      </Suspense>
      <Suspense fallback={<div>Loading...</div>}>
        <Data2 />
      </Suspense>
      <Suspense fallback={<div>Loading...</div>}>
        <Data3 />
      </Suspense>
    </div>
  )
}

Now this snippet does what we want. As the data is available, it will render. If a particular API call is slow, it will show the fallback until the data is available.

If you're wanting to create a page-level fallback, you can use the loading.js file convention described in the Next.js docs.

So again, if you find yourself reaching for Promise.all in RSCs, you probably want Suspense instead.

If you want to level this up even more, you can use Partial Prerendering in Next.js 14 to render the fallbacks as part of the initial content.

Watch Lee's YouTube video on Partial Prerendering to learn more.

Stay up to date

Don't miss my next essay — get it delivered straight to your inbox.

Related Content