I'm working on a pretty standard Next.js 13.5.6 project which utilises app router directory with Strapi CMS as a backend. Project is NX monorepo, if that matters.
I have a page which renders a list of articles with filters and pagination where I want streaming to occur. Project structure is nothing special, page itself is in:
/app/[locale]/articles/news/page.tsx
[locale] folder is used for next-intl, which is pretty standard library as well. Page code goes as follows:
import { ArticlesLayout } from "@/components/layouts";
import { Header, Footer } from "@/components/widgets";
import { getArticlePage } from "@/api";
type PageProps = {
// ...
}
export default async function Page ({ params: { locale }, searchParams }: PageProps) {
// getting Page specific data: page header, footer, breadcrumbs, etc.
// function itself is just a wrapper around Next's fetch with preconfigured path and auth params
const data = await getArticlePage({ locale });
return (
<>
<Header props={data.header} />
<ArticlesLayout locale={locale} searchParams={searchParams} />
<Footer props={data.footer} />
<>
);
}
So the page itself fetches some data, renders Header and Footer components and passes some data like searchQuery and pagination to ArticlesLayout. Code again follows the documentation:
import { Suspense } from "react";
import { ArticleFilter } from "@/components/widgets";
import { ArticleList } from "@/components/listings";
import { SkeletonList } from "@/components/listings";
type LayoutProps = {
// ...
}
export async function ArticlesLayout({ locale, searchParams }: LayoutProps) {
const query = searchParams.query;
return (
<section>
<ArticleFilter />
<Suspense key={query} fallback={<SkeletonList />}>
<ArticleList query={query} />
</Suspense>
</section>
);
}
Instead of using loading.tsx for the whole page (since Page itself fetches data only once and almost imediately) I want to stream the suspended content so according to the docs I import Suspense from React and wrap my ArticleList component with it, passing SkeletonList as a fallback.
ArticleFilter is a client component which uses useSearchParams hook to modify searchParams depending on checkbox set. And the ArticleList itself is another server component that fetches the data:
import { getArticles } from "@/api";
export async function ArticleList({ query }: { query: string }) {
const articles = await getArticles(query);
return (
<>
{/* list markup here */}
<>
);
}
Clicking a checkbox modifies url, that makes a new request. I expect that while await inside ArticleList is in effect, it will be replaced by a SkeletonList since it was passed to Suspense as a fallback. But instead nothing happens until promises is awaited and then the list just rerenders immediately with new data.
I tried passing revalidate: 0 to fetch, using cache: 'no-store' and even upgrading to Next 14 to add unstable_nostore to add noStore() call to my fetch functions. It doesn't help. What can be the cause of such behavior and how do find the reason of it? Maybe there's a way to debug why suspense isn't taking effect?