-
Notifications
You must be signed in to change notification settings - Fork 27.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updating search params does not trigger suspense fallback or loading.tsx #53543
Comments
Hello, i don't know if this is an expected behavior or not, but i've stumbled on this behvior a little while ago and traced it back to React ( link : https://react.dev/reference/react/Suspense#resetting-suspense-boundaries-on-navigation ). The thing is that for My solution was just to include the searchParams into the key of the export default function Page(props: {
searchParams?: Record<string, string | undefined>;
}) {
const keyString = `search=${props.searchParams?.search}`; // <-- Construct key from searchParams
return (
<section>
<SearchInput />
{props.searchParams?.search && (
<React.Suspense
// pass key to Suspense
key={keyString}
fallback={
<ul>
<ProductCardSkeleton />
<ProductCardSkeleton />
<ProductCardSkeleton />
<ProductCardSkeleton />
</ul>
}
>
// <ProductList> is an async React component that do the fetching
<ProductList name={props.searchParams?.search} />
</React.Suspense>
)}
</section>
); This won't work for |
Thanks for the response @Fredkiss3 ! I'm sure it will help someone. Unfortunately for my scenario that won't trigger the suspense boundary because I am loading new data via the a react server component. I would think the loading.tsx file would hit the fallback since that is the suspense boundary of my page.tsx. |
The key thing seems to be how it is supposed to work : Normally if you want to trigger suspense boundaries, you have to fetch your data in a subcomponent not the page directly, this is fairly easy as you can pass props from the page to another component. |
Thanks for the tweet thread, I hopped in the replies. Since NextJS is handling the boundaries, there should be a way to re-trigger the suspense boundary, whether that's giving loading.tsx knowledge of the params, or having search params be included in the keys. I'm not sure what @sebmarkbage means that search params are generally used differently. If I'm not using them correctly in my bug, I can refactor to use params for my pagination. But having something like edit: I've updated the sandbox to include a refactored version of pagination using [pageNumber]. |
One thing @dclark27 CodeSandbox does not support streaming, so suspense won't work as expected. |
I've encountered the same issue. If the API has a cold start when performing a search, I would like to show the "loading" state from loading.tsc file till the current request is finished. |
@Fredkiss3 did you test it in production too ? because it worked for me in dev mode but in production it didn't |
I've got the same issue Suspense isn't being triggered for searchParams at all. @dclark27 did you find a solution for this problem ? |
Got the same problem. I tried @Fredkiss3 's solution by passing a key to but it didn't work, not even in dev stage. |
Having the same problem. In a statically generated component, I have a Suspense boundary with a param-based key over a RSC that renders data based on query parameters from page.tsx. It also has noStore() to make the RSC dynamic. Fallback does render when query parameters change in development mode but not in production. Don't really know what to do from this point. |
Hi all, I tried a workaround and it works for me. Please try to convert your page.tsx to a client component using Hope it helps. Thanks.
"use client";
import { Suspense } from "react";
import BlogSkeleton from "../../../components/ui/BlogSkeleton";
import AllBlogPosts from "./AllBlogPosts";
import { useSearchParams } from "next/navigation";
import BlogLayout from "../../../components/section/BlogLayout";
const Blog = () => {
const searchParams = useSearchParams();
const searchParamsValue = searchParams.has("type")
? searchParams.get("type")
: "all";
const keyString = searchParamsValue ? searchParamsValue : "all";
return (
<BlogLayout>
<Suspense key={`type=${keyString}`} fallback={<BlogSkeleton />}>
<AllBlogPosts searchParamsValue={keyString} />
</Suspense>
</BlogLayout>
);
};
export default Blog; |
Unfortunately, this solution wastes all the benefits of using server components. |
I somewhat agree with you. However, given that my app is kind of light-weighted and it's acceptable for me to shift the rendering to client side. |
@anthonyiu if you specify the whole page as client component, then how can you fetch data in AllBlogPosts as it's no longer a server component? Or do you mean you fetch data manually using API? |
hey did anybody found the solution? |
use key in suspense component and the key should be unique, for my case, the key was api's url with the searchparams in string format |
Adding the key in Suspense worked for me, but then turned the entire route dynamic. I was hoping to use partial prerendering, but that only works if I remove they key from Suspense, which then causes the fallback to not trigger lol. const Page = ({ searchParams }) => {
return (
<>
<Suspense key={JSON.stringify(searchParams)}>
<DynamicContent searchParams={searchParams} />
</Suspense>
<StaticContent />
</>
)
} This works visually as expected, but causes the whole route to be dynamic Edit: This only worked locally, and did not work when deployed. |
For people that are still not able to get this to work correctly, here is what worked for me. Some context before we move on. The react documentation states the following:
So with that in mind, make sure that the following is true: 1 - The actual data loading is happening in the child, and not the same component where the Suspense component lives. NOTES: Although I am unsure of the consequences of adding a key to a Suspense component, this seemed to work well and solved the issue I was having. Maybe this is an anti pattern for Suspense specifically? Time will tell. Both of the components shown below are server components. I am unsure if applying the same strategy would work if the child component was a client component, but from the react documentation it states the following:
So it should work for client components, if the conditions above are met. Here is an example: // page.tsx
// note that this is a server component
import { Suspense } from "react";
import { ListingGrid } from "@/app/_listing/ListingGrid";
import ListingGridFilter from "@/app/_listing/ListingGridFilter";
import { ListingSearchParams } from "@/app/lib/listing/data";
import ListingGridSkeleton from "@/app/_listing/ListingGridSkeleton";
export default async function Home({
searchParams,
}: {
searchParams: ListingSearchParams;
}) {
const beds = searchParams?.bedrooms ?? "0";
const baths = searchParams?.bathrooms ?? "0";
const park = searchParams?.parking ?? "0";
const price = searchParams?.price ?? "0";
return (
<main className="flex -h-screen flex-col items-center justify-between p-24">
<ListingGridFilter />
<Suspense
key={beds + baths + park + price}
fallback={<ListingGridSkeleton />}
>
<ListingGrid searchParams={searchParams} /> // <--- Data loading is happening here
</Suspense>
</main>
);
} // ListingGrid.tsx
// note that this is also a server component
import {
ListingData,
ListingSearchParams,
ListingType,
} from "@/app/lib/listing/data";
import { ListingCard } from "./ListingCard";
import { unstable_noStore } from "next/cache";
export async function ListingGrid({
searchParams,
}: {
searchParams: ListingSearchParams;
}) {
// This is just to force requests to be fetched every time
// so you can see the components loading.
unstable_noStore();
const listing: Array<ListingType> = await ListingData(searchParams);
if (!listing.length) {
return <p className="text-xl">No listing matches your criteria</p>;
}
return (
<div className="w-3/4 grid grid-cols-1 2xl:grid-cols-4 xl:grid-cols-3 lg:grid-cols-2 gap-y-12 gap-x-4">
{listing.map((listingItem) => (
<ListingCard key={listingItem.Id} listing={listingItem} />
))}
</div>
);
} |
I made an in depth reproduction of my use case here. Repo: https://github.com/christopher-caldwell/ppr-loading-suspense-demo Adding a key to suspense works locally, but NOT hosted on Vercel. I haven't tried
So I cannot really tell if this behavior is intentional or not. |
@ubirajaramneto Adding With Suspense key: https://ppr-loading-suspense-demo.vercel.app/?page=1 I mostly want to know if this behavior is considered intentional for where the dev team is in the development process of PPR. I understand that features will trickle in, but is this supposed to be happening? At least for now. Edit: |
@christopher-caldwell I have uploaded the whole project in my github for you to see, also just to make sure, I have deployed the application to vercel and it is working the same way as it is working on localhost. Demo: https://nextjs-listings-app.vercel.app/ After reviewing your code, many things popped in my head as what the culprit would be, but I am not confident enough to take a bet. But if I was forced to guess, I would try the following: 1 - remove the wait promise that you wrapped your api call around Try to read through my example and try applying some of the patterns you see there and see if anything works out. |
@ubirajaramneto Thanks for your suggestions. I appreciate you taking a look. I have implemented them, but didn't seem to change anything: https://ppr-loading-suspense-demo-git-ubira-e4da36-christopher-caldwell.vercel.app/ and https://github.com/christopher-caldwell/ppr-loading-suspense-demo/blob/ubirajara-suggestions/app/page.tsx Something to note, your project does not have the For the wait, it is just to simulate the network taking longer than expected. It doesn't wrap the call, I put in a support ticket with Vercel, as this works when I run it on my computer with |
Sorry @christopher-caldwell , my solution was not necessarily aiming at your problem, since I am not doing any pre-rendering in the example I provided. This example I posted was to point the general public on how to handle the suspense boundary not triggering, which was the problem I first faced and wanted to share my solution here so it can serve as a reference. And regarding your await call, yes you are right, I might have expressed myself poorly on that instance. If you like you can contact me directly and I'll be glad to throw some insight into your problem, just so we can keep this issue clean of parallel discussions. Hope you can find a solution for this problem soon! |
Interestingly, your solution worked perfectly for me locally. So I reached to Vercel, because they say that if your code is working locally, and on their platform, you should submit a support ticket. The only blurb about PPR is that it doesn't support client side navigation, which is a bit ambiguous to me. So I'm not sure if my issue is because of PPR (which is fine, it's experimental) or Vercel is not supporting it or whatever the case is. |
If anyone else runs into this issue (specifically with PPR), using regular html anchor tags worked for me. You don't get all the benefits of the Next I also heard back from Vercel. Even if it works locally, they say to take it up with Next since it's an experimental feature. It's not their problem, according to them (they were nice, I don't mean to imply they weren't). |
face the same issue, working fine on local build if any one solve the issue, please share it can be helpful |
Any updates from the next.js team from this? I would have assumed adding a loading.tsx file would mean that if the page is fetching more data on navigation change (search params changes etc) that the loading.tsx would be re-rendered? |
This really needs to be addressed, can't use server-side data fetching for a data-intensive chart now :( |
@AChangXD you can just put the data fetching into an async component that you then wrap in Suspense in your page.tsx file. You can then add the fallback to the suspense instead of using loading.tsx and it works. NOTE: if you're using search params to navigate you do have to put a key on the Suspense that changes, so that it actually triggers the fallback though for example: key={`${searchParams.query}${searchParams.otherParam}`} |
Yeah I didn't realize the key needs to be dynamic, working now😃 |
I have similar problem. Do you have an example how to do this? When you say you can just put the data fetching into an async component, Currently my data fetched in the Server component, moving to a seperate component means what the data should I return? This is how my current component looks export default async function Transactions({
searchParams,
}: {
searchParams?: {
fromDate?: string;
toDate?: string;
transferType?: string;
status?: string;
beneficiary?: string;
order?: string;
};
}) {
const t = await getTranslations();
const fromDate = searchParams?.fromDate || null;
const toDate = searchParams?.toDate || null;
const transferType = searchParams?.transferType || null;
const status = searchParams?.status || '';
const beneficiary = searchParams?.beneficiary || null;
const order = searchParams?.order || 'DESC';
let filters: TransactionListFilters = {
order: order as 'ASC' | 'DESC',
};
if (fromDate) {
filters['from_date'] = fromDate;
}
if (toDate) {
filters['to_date'] = toDate;
}
if (transferType) {
filters['trans_type'] = transferType as TransType;
}
if (status) {
filters['status'] = status as Status;
}
if (beneficiary) {
filters['beneficiary_id'] = beneficiary;
}
// Data fetching
const transactions = await transactionService.getTransactionList(filters);
// data fetching
const beneficiaries = await beneficiaryService.getBeneficiaryList();
return (
<Box>
<Container>
<TransactionList
transactions={transactions?.transactions}
appliedFilters={Object.keys(searchParams!).length ?? 0}
beneficiaries={beneficiaries?.beneficiaries}
/>
</Container>
</Box>
);
} |
@remitone that should be fine, just wrap that component in suspense where ever youre rendering it, and add they key prop |
for me in Next v14.2.3 the page render was not even triggering going from having a const handleSearch = (term: string) => {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('search', term);
} else {
// This is a workaround for a bug where the page without a search query
// would not trigger a RSC update.
// params.delete('search');
params.set('search', term);
}
replace(`${pathname}?${params.toString()}`, { scroll: false });
}; |
* site: implement flexsearch * site: add no search results banner * site: work around empty searchParams Next.js bug? related vercel/next.js#53543
Are there any fixes in site? Many threads about this that have been open for 1+ years. |
Also hoping for a real solution |
I use startTransition like that.
If isPending is true ,I show Loading component. |
@christopher-caldwell Unfortunately, this is still the case with Next.js 15 Canary and experimental PPR. – Did you ever found a workaround/solution? cc @leerob |
If the behavior you are looking for is this:
Then we now have an example working on canary with |
Hey @leerob, first: Wow, what a fast & helpful reply. You're an absolute OG. 🫶 Using In my case it's a simple pagination I want to get to work, I rebuilt the navigation buttons like that (I gave them each their separate // Get the `page` param (type-safely via nuqs)
const [page] = useQueryState('page', parseAsInteger.withDefault(1))
return (
<PaginationFormAction page={page} newPage={Math.max(page - 1, 1)}>
<ChevronLeft className="h-4 w-4" />
</PaginationFormAction>
<PaginationFormAction page={page} newPage={Math.min(page + 1, totalPages)}>
<ChevronRight className="h-4 w-4" />
</PaginationFormAction>
) interface PaginationFormActionProps extends HTMLAttributes<HTMLFormElement> {
page: number
newPage: number
}
const PaginationFormAction = ({ children, page, newPage }: PaginationFormActionProps) => {
return (
<Form action="" replace={true} scroll={false}>
<input type="hidden" name="page" value={newPage} />
<Button variant="outline" className="h-8 w-8 p-0" disabled={page === newPage}>
{children}
</Button>
</Form>
)
} The asynchronous server components that perform the data fetching are wrapped in a Suspense key-ed with the search params (as suggested above): <Suspense
key={JSON.stringify(props.searchParams)} // Note: Using `props.searchParams` here as I pass down `searchParams` into a nested server component and make use of PPR
fallback={<Skeleton />}
>
<Items />
<Pagination />
</Suspense> And voilà: It work's as it should. 🎉 But only locally (both via The live version deployed to Vercel (same node version) just doesn't show the Suspense fallback at all. The site is frozen/blocked until the fresh data is awaited. It basically yields to the exact same problem I, and some others above like @christopher-caldwell, have had before just with a different approach (using Have you tried deploying the commerce example? Is Vercel not yet ready for PPR (even though it's in experimentation-state for 1 year)? |
The above code is deployed here https://demo.vercel.store and working on Vercel. CleanShot.2024-09-04.at.09.08.57.mp4 |
Hmm, unfortunately I can't try it myself as I don't have a Shopify Storefront and the whole setup seems a bit too cumbersome for just debugging this. Nonetheless, I'll try to find differences in the implementations (mine shared above still looks right to me). What I can see right away is that the example, even though it runs on |
Yes, Suspense and PPR (although experimental) are working on Vercel 👍 |
I'm confused by the behavior Suspense has on Server components and don't know how much of it is working as intended. Some people here suggest that there are workarounds, but I haven't found any for my specific problem. I created https://github.com/nachogutierrez/nextjs-suspense-bug-example to explain what I mean. I use ValueSetter to represent client components that update search params in the URL, and ContentLoader to represent an async server component that fetches data and then renders something (note: to simulate latency, ContentLoader always takes 2 seconds to render). Lastly there's only one page which renders two value setters for search params 'A' and 'B', and 1 or 2 content loaders depending on which case it is (see below). I created branches
@leerob I gave the new Form component in canary a try but I don't think it solves the problem I have with case-3. I created branch |
I tested the canary version with the new Form component, but I still have the same results that using useRouter() API, I'm testing it with 3G connection and it takes 2s for the server to respond and during that time no loading status is shown (using the suspense component). In my case I think the issue is because during those 2s the routes are not changing and the component |
@nachogutierrez yeah this restriction is unfortunate, but it's how native also note that the only solution for preserving search params (that i'm aware of) is to render an
it's not always convenient, but |
Does not work on vercel at this moment. Typing in the search bar does nothing, doesn't update the URL either. |
@csmartinsfct you need to press enter – we could probably change this to be onChange. |
This comment has been minimized.
This comment has been minimized.
I confirm @leerob's solution works on nextjs 15.1.2, deployed to Vercel.
|
Hi, I'm also facing a similar situation. I have a base route Edit 1: The link does work if change to a plain tag but then Suspense isn't used a full page refresh happens in the base route export const experimental_ppr = true
export default async function Blog({
searchParams
}: {
searchParams: Promise<{ status: "published" | "archived" }>;
}) {
return (
<Suspense key={`blog-header`} fallback={<div>loading button</div>}>
<BlogHeader searchParams={searchParams} />
</Suspense>
...
)
} where export default async function BlogHeader({
searchParams
}: {
searchParams: Promise<{ status: "published" | "archived" }>;
}) {
const status = (await searchParams).status === "archived" ? "archived" : "published"
return (
<div className="flex items-center justify-between">
<Link
href={`/blog?status=${status === "archived" ? "published" : "archived"}`}
className="ml-auto mr-2 rounded-md bg-tokyo-night-cyan px-4 py-2 text-base text-tokyo-night-background transition-colors hover:bg-tokyo-night-cyan/80"
>
Toggle {status === "archived" ? "Latest" : "Archived"} Posts
</Link>
</div>
);
} Also I'm not sure if it's relevant but here is the parallel route logic as well export default async function Blog({
searchParams
}: {
searchParams: Promise<{ status: "published" | "archived" }>;
}) {
return (
<Suspense key={`blog-list`} fallback={<div>loading...</div>}>
<BlogList searchParams={searchParams} />;
</Suspense>
)
} The |
@Tanish2002 put the search params in the key of the suspense... <Suspense key={`blog-list-${searchParams.status}`} you'll need to await them though |
That doesn't seem to work..
export const experimental_ppr = true
export default async function Blog({
searchParams
}: {
searchParams: Promise<{ status: "published" | "archived" }>;
}) {
const awaitedSearchParams = await searchParams;
return (
<Suspense key={`blog-list-${awaitedSearchParams.status}`} fallback={<div>loading...</div>}>
<BlogList searchParams={awaitedSearchParams} />;
</Suspense>
)
}
export const experimental_ppr = true
export default async function Blog({
searchParams
}: {
searchParams: Promise<{ status: "published" | "archived" }>;
}) {
const awaitedSearchParams = await searchParams;
return (
<div className="prose-xl my-auto space-y-4 prose-p:my-0">
<Suspense key={`blog-header-${awaitedSearchParams.status}`} fallback={<div>loading button</div>}>
<BlogHeader searchParams={awaitedSearchParams} />
</Suspense>
... When I click on the link inside Also since I'm awaiting the |
Verify canary release
Provide environment information
Operating System: Platform: linux Arch: x64 Version: #22 SMP Tue Jan 10 18:39:00 UTC 2023 Binaries: Node: 18.14.2 npm: 9.5.0 Yarn: 1.22.19 pnpm: N/A Relevant Packages: next: 13.4.12 eslint-config-next: 13.4.12 react: 18.2.0 react-dom: 18.2.0 typescript: 5.0.4 Next.js Config: output: N/A
Which area(s) of Next.js are affected? (leave empty if unsure)
App Router, Routing (next/router, next/navigation, next/link)
Link to the code that reproduces this issue or a replay of the bug
https://codesandbox.io/p/sandbox/muddy-glade-hn895s
To Reproduce
Describe the Bug
Updating search params does not trigger suspense fallback or loading.tsx
Expected Behavior
Fetching new data based on new search params trigger suspense fallbacks or loading.tsx
Which browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
No response
The text was updated successfully, but these errors were encountered: