Next.js Discord

Discord Forum

Suspense not working

Unanswered
Spectacled bear posted this in #help-forum
Open in Discord
Avatar
Spectacled bearOP
I'm using next 14.0.4.
I have a server component called ResultsGrid that fetches some data, which it then displays. This component is then wrapped in a Suspense component which has a fallback component.

As far as I understand, the way this should work is that everything outside the Suspense boundary (which is static) should render along with the fallback component, up until the fallback component is replaced with the Result component when the data is fetched and is ready to render.

However, in my case, that does not happen, and the whole page just loads until the data is fetched (around 5 seconds because of the timeout) and the whole page is ready at once. What could be the issue?
i tried following every basic Suspense tutorial out there and it never works for me.

This is my ResultsGrid component which needs some loading time:
import { getRandomFilm } from "@/lib/film";
import FilmResult from "./FilmResult";

async function getFilms(n: number) {
  let films: Film[] = [];
  for (let i = 0; i < n; i++) {
    films[i] = await getRandomFilm();
  }
  await new Promise((resolve) => setTimeout(resolve, 5000));
  return films;
}

export default async function ResultGrid({
  searchParams,
}: {
  searchParams: ExploreSearchParams;
}) {
  const films = await getFilms(8);

  return (
    <div className="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))]">
      {films.map((film) => {
        return <FilmResult key={film.slug} film={film} />;
      })}
    </div>
  );
}


and this is the page with the suspense boundary:
import { Suspense } from "react";
import ResultGrid from "@/components/explore/ResultGrid";

export default function Explore({
  searchParams,
}: {
  searchParams: ExploreSearchParams;
}) {
  return (
    <main>
      <Suspense
        fallback={
          <div className="text-5xl font-bold text-red-600">LOADING...</div>
        }
      >
        <ResultGrid searchParams={searchParams}></ResultGrid>
      </Suspense>
    </main>
  );
}

18 Replies

Avatar
Spectacled bearOP
I create a completely clean next.js project using npx create-next-app@latest, I followed another very simple tutorial and guide where I just wrap an async server component that awaits a promise which resolves after a few seconds before returning a react component, into a Suspense component.

Still doesn't work as in the guides/tutorials. The page loads until it renders the whole thing, not displaying the fallback in the Suspense component at all.
Avatar
try this
await new Promise((resolve) => setTimeout(resolve, 5000 * Math.random()));
Avatar
Spectacled bearOP
Same thing... Only the loading is now random.
I just figured out that it works on chrome
The way I did it too.
But not on safari, at all
So it's a browser issue somehow.
Avatar
oh i can see your issue now
Avatar
Spectacled bearOP
For anyone who searches for this info later:
Safari doesn't work well with Suspense if the first render is too small, (according to this explanation https://github.com/vercel/next.js/issues/52444)
Avatar
export default async function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      {Array.from({ length: 8 }).map((_, i) => (
        <Suspense key={i} fallback={<div>loading...</div>}>
          <Child id={i} />
        </Suspense>
      ))}
    </main>
  );
}

async function Child({ id }: { id: number }) {
  await new Promise((resolve) => setTimeout(resolve, 5000 * Math.random()));

  return <div>{id}</div>;
}

try this
Avatar
Spectacled bearOP
Works perfectly in chrome (separate child components render at different times, until then loading... is show)

Safari just renders the final state of the page at once without showing anything before it.
Just the numbers.
Avatar
can you try this link on safari?
https://streaming-phi-hazel.vercel.app/section
Avatar
Spectacled bearOP
Interesting. It does look like it's working. When I click one of the numbers there are sections where loading... appears then is replaced or is gone.
Avatar
yea the loading... is from loading.tsx
the loading... on the top left
Avatar
Spectacled bearOP
The posts also load dynamically lazily (not sure what the proper term is)
Avatar
import { render } from "@react-email/render";
import { Suspense } from "react";

export const dynamic = "force-dynamic";

export function generateStaticParams() {
  return Array.from({ length: 8 }).map((_, i) => ({ name: String(i + 1) }));
}

export default async function Home({ params }: { params: { name: string } }) {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      {Array.from({ length: 8 }).map((_, i) => (
        <Suspense key={i} fallback={<div>loading...</div>}>
          <Child name={params.name} />
        </Suspense>
      ))}
    </main>
  );
}

async function Child({ name }: { name: string }) {
  const markup = await getComponent(name);
  await new Promise((resolve) => setTimeout(resolve, 5000 * Math.random()));

  return <div dangerouslySetInnerHTML={{ __html: markup }}></div>;
}

async function getComponent(name: string) {
  const Email = (await import(`../../../email/${name}/twitch`)).default;

  const markup = await new Promise((resolve) =>
    resolve(render(<Email />, { pretty: true }))
  );
  // const markup = render(<Email />, { pretty: true });
  return markup as string;
}

this is the code
streaming