Suspense not working
Unanswered
Spectacled bear posted this in #help-forum
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:
and this is the page with the suspense boundary:
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
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.
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.
try this
await new Promise((resolve) => setTimeout(resolve, 5000 * Math.random()));
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.
oh i can see your issue now
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)
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)
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
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.
Safari just renders the final state of the page at once without showing anything before it.
Just the numbers.
can you try this link on safari?
https://streaming-phi-hazel.vercel.app/section
https://streaming-phi-hazel.vercel.app/section
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.yea the loading... is from
loading.tsx
the loading... on the top left
Spectacled bearOP
The posts also load dynamically lazily (not sure what the proper term is)
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