Go back

Here's how to use React Suspense in Next.js

Recently, I tried using React Suspense in a Next.js project, only for me to discover that there is no support for Suspense in Next.js — or at least the pages directory.

If you're using the appDir of Next.js, this support is extended, as there's a feature, Streaming and Loading UI that is built on top of React Suspense.

I use the pages directory for all of my projects, and I needed to really use Suspense. But, every time I try doing that, I'll get an error like the one below.

Error: Fallback data is required when using suspense in SSR

For a while, I couldn't wrap my head around why this error kept showing up. To make matters worse, I was using SWR too, and their docs also specified that using <Suspense> in data frameworks like SWR isn't recommended.

They went on to talk about the drawback of accomplishing this in SSR frameworks like Next.js too. When I saw this, I was just weak — nonetheless, I pressed on.


After scouring the internet vainly, I finally stumbled upon Ali Bentaleb's article on how to use Suspense with next/dynamic, since it — next/dynamic — is a bit similar to React.Lazy().

He also suggested the use of React concurrent mode by updating your next.config.js file with the snippet below.

module.exports = {
  reactStrictMode: true,
  concurrentFeatures: true,
}

I tried this approach, and I still do not know why it didn't work as expected. Later on, I realized it could be because I didn't include the concurrentFeatures property in my config.


The snippet below was what worked for me. If you take a closer look, you'll notice that I added the ssr property and set its value to false, and because I use SWR, I set the suspense value to true in the config

pages/dashboard.tsx
import { SWRConfig } from 'swr'
import dynamic from 'next/dynamic'
 
const Dashboard = dynamic(() => import('@containers/dashboard'), {
  suspense: true,
  ssr: false,
})
 
const DashboardPage = () => {
  return (
    <>
      <Head>
        <title>Dashboard</title>
      </Head>
      <SWRConfig value={{ suspense: true }}>
        <Suspense fallback={<LoadingComponent />}>
          <Dashboard />
        </Suspense>
      </SWRConfig>
    </>
  )
}
 
export default DashboardPage

There's one catch with this approach though. By setting ssr: false, I'm deferring the rendering of the component to the client-side, which can potentially impact the initial load time and SEO considerations.

But, since this route is a protected one — a user dashboard — The SEO caveat can be ignored.

If you want to use suspense on a page that is SEO dependent, I'll suggest that you make use of the concurrentFeatures approach that Ali suggested in his article

You can also learn more about Suspense and Server-Side Rendering in this GitHub issue if you want to — because some errors can vary depending on our use cases.

A potential fix

One thing I realized after using this approach where i had to disable ssr by setting its value to false is that, upon refreshing or reloading the page, the error still persists.

Sometimes it shows up in production where the page is black with an error similar to the one below

A client-side exception occured. check the browser console for...

So many issues were brought up regarding React Suspense in Next.js. Although, the Next.js team offered a workaround for using Suspense with React.lazy() instead of next/dynamic.

This Pull Request covers everything they fixed regarding Suspense. Take a look, when you can.

I stumbled upon another clue that seemed like a fix — and yes, it became one on the long run. It was suggested by Kota Zhang in this discussion.

This approach suited my usecase because I had data that keeps on chnaging at differnet intervals. If you happen to be using a data-heeavy library like SWR, you may undertand why this keeps happening.

It is somewhat related to how the data changes, and what is rendered on the client is different from that of the server.

Hence, appending a unique key property to the Suspense component to distinguish these renders would suffice. In the snippet below, I employed randomUUID from the crypto module in Typescript.

If you're not using Typescript, you may have to install the module/package.

pages/dashboard.tsx
import React, { Suspense, lazy } from 'react'
import useSWR, { SWRConfig } from 'swr'
 
const Dashboard = dynamic(() => import('@containers/dashboard'), {
  suspense: true,
})
 
const Index = () => {
  const susKey = crypto.randomUUID()
 
  return (
    <>
      <SWRConfig value={{ suspense: true }}>
        <Suspense key={susKey} fallback={<Loader />}>
          <Dashboard />
        </Suspense>
      </SWRConfig>
    </>
  )
}
 
export default Index

In this article