Cache Components: Is there per route ppr?
Unanswered
Giant panda posted this in #help-forum
Giant pandaOP
Have a route that uses a dynamic api (cookies) but when enabling cache components, it requires wrapping the entire route in a suspense. Is there no way to disable cache component/ppr for this route?
I wasn't aware that dynamic routes like these still need to be wrapped in a suspense. I would prefer if it was instead blocking because the addition of the suspense leads to the page loading faster but would first flash the suspense fallback which is not what I want.
I wasn't aware that dynamic routes like these still need to be wrapped in a suspense. I would prefer if it was instead blocking because the addition of the suspense leads to the page loading faster but would first flash the suspense fallback which is not what I want.
163 Replies
Saint Hubert Jura Hound
Yeah this is something i was also thinking about when switching to cache components. I have a auth provider wrapped around the root layout which suspends. But i just went with the suspense fallback and noticed since its in a layout the fallback actually only shows when first visiting the site or on full page refresh. So subsequent navigations are very fast and dont show the fallback. So unless someone here has a different solution ill probabably just stick with the fallback
You don't have to wrap the entire route in suspense. What you need to do is access the data at a lower value
So for example you could start the promise on your page, not await it and then pass it to a child component and then wrap the child component in suspense and use use() or await to access the data
This allows the rest of the page that's not dynamic to be partially pre-rendered and if you're using vercel will be served from a CDN
The idea of cache components in PPR is to get as much content to the screen as possible. So you can get a lot of the UI showing and then show a fallback for a smaller part of the UI instead of the whole route
You can also cache the dynamic content too
That's the classic diagram showing how PPR works with dynamic content
@Saint Hubert Jura Hound there are ways around that, if you could make the provider a client component that would work. But I know clerk has a provider and it still works with the cache components and not having to wrap the provider in suspense
@Patrick MacDonald You don't have to wrap the entire route in suspense. What you need to do is access the data at a lower value
Giant pandaOP
Sorry I don't think I was clear enough with my issue. I want this specific route to be entirely dynamic since content depends on auth state and search params.
But apparently you can't have a blocking dynamic route without wrapping the entire route in a suspense when cache components are enabled. But that then leads to rendering of the fallback initially which I don't want. The solution for now seems to be wrapping the body in a suspense. More information here: https://github.com/vercel/next.js/issues/86739
Another issue I noticed is that with cache components enabled, useSearchParams on the client still leads to build errors if the client component is not wrapped in a suspense evne though it's a client side hook.
But apparently you can't have a blocking dynamic route without wrapping the entire route in a suspense when cache components are enabled. But that then leads to rendering of the fallback initially which I don't want. The solution for now seems to be wrapping the body in a suspense. More information here: https://github.com/vercel/next.js/issues/86739
Another issue I noticed is that with cache components enabled, useSearchParams on the client still leads to build errors if the client component is not wrapped in a suspense evne though it's a client side hook.
Yes my friend look at the diagram I sent you. You can have dynamic parts of the route while still maintaining a static shell
Anything that relies on auth can be dynamically loaded like a hole inside of the static shell
@Giant panda you don't need the entire route dynamic. You can have dynamic parts to the route. You don't need to wrap the entire route in suspense AKA loading.tsx
You just wrap the components that are using auth in suspense. You can even use the same auth promise in multiple places if you start it on the page, but don't await it until you are in a child component that is wrapped in suspense
export default function SettingsPage() {
const settingsPromise = getSettings();
return (
<div className="container mx-auto py-8 px-4 flex-1 max-w-2xl animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="mb-8">
<h1 className="text-3xl font-bold tracking-tight text-primary">
Settings
</h1>
<p className="text-muted-foreground">
Configure application wide triggers and intervals.
</p>
</div>
<Suspense fallback={<SettingsFallback />}>
<SettingsForm initialSettingsPromise={settingsPromise} />
</Suspense>
</div>
);
}settings uses auth but its just a dynamic part in a static shell
@Patrick MacDonald You just wrap the components that are using auth in suspense. You can even use the same auth promise in multiple places if you start it on the page, but don't await it until you are in a child component that is wrapped in suspense
Giant pandaOP
Right I understand that point but what I don't want for this specific route is a flash of the fallback from the suspense. There is no static shell to be had for this page. It's entirely dynamic.
you cant make the dynamic part of the component that acesses that data dynamic?
Giant pandaOP
The entire page is dependent on auth state and search params.
one sec
export default async function InvoicesPage() {
const invoicesPromise = getInvoices();
return (
<div className="container mx-auto py-10 px-4">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold">Invoices</h1>
<p className="text-muted-foreground">
Managing your billing and payments.
</p>
</div>
<Button asChild>
<Link href="/invoices/new" className="flex items-center gap-2">
<PlusCircle className="h-4 w-4" /> Create Invoice
</Link>
</Button>
</div>
<Suspense>
<InvoicesTable invoicesPromise={invoicesPromise} />
</Suspense>
</div>
);
}you can access search params to if you wanted this way
export default async function InvoicesPage(props:PageProps<"/invoices">) {
const invoicesPromise = props.searchParams.then((params)=>getInvoices(params.id);)
return (
<div className="container mx-auto py-10 px-4">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold">Invoices</h1>
<p className="text-muted-foreground">
Managing your billing and payments.
</p>
</div>
<Button asChild>
<Link href="/invoices/new" className="flex items-center gap-2">
<PlusCircle className="h-4 w-4" /> Create Invoice
</Link>
</Button>
</div>
<Suspense>
<InvoicesTable invoicesPromise={invoicesPromise} />
</Suspense>
</div>
);
}only the part where you accese the params is dynamic
Giant pandaOP
Right but in this example, it would have a build error since cache components require dynamic segments to be wrapped in a Suspense.
<Suspense>
<InvoicesTable invoicesPromise={invoicesPromise} />
</Suspense>
<InvoicesTable invoicesPromise={invoicesPromise} />
</Suspense>
it is
the page is static becuase there is no await nor is it aysnc
only the invoice table is considered dynamic
Giant pandaOP
But what I'm trying to say is that the entire page is reliant on that dynamic data.
yes and where you use that data
will; be dynamic
and the page will be static will hole for dymina content
where evere the prmose resolves has to be wrapped in suspense
in my example the page doesnt not resole the promise so it is part of the static shell
for example you use the users name you only need to wrape the part that displays the name is suspense
Giant pandaOP
right but then there would be a flash of the fallback from the suspense which is not what I want for this route. I don't want loading skeletons/spinners for those dynamic "holes". I want everything to load at once.
there is no need for the whole page to be dymainc
in the example i show
the whole pages is shown and you can choose to show a fall back for the dynamic part
so in the example i show the only part that flashes or loads a fall back is the invoice table
@Giant panda Right but in this example, it would have a build error since cache components require dynamic segments to be wrapped in a Suspense.
there is not build error with cache componets in this example'
Giant pandaOP
But in this instance, the ui layout and basically all of the content on the page is dependent on that dynamic data. So if I go with that solution, I would effectively have then the entire page show a fallback initially.
which is not what I want
I think you are looking at it wrong
if the data is dynamic you will show nothing or you can show a fallback that shows a loading state
either way there is no need for the whole route to be dynamic
you can use suspense and just nto show a fall back
then you have the same effect of when the data is loaded it shows up
Giant pandaOP
Right but since the entire page ui depends on this dynamic data, I would effectively have one giant loading skeleton/blank page on initial load which is not what I want.
my friend
the lay out will load
the nav bar
the footer
the whole point of ppr is to get as much to the screen while we wait for the data to fetch
yous hopuld never have a blank page
look at the digram you are getting as much content to the screen as you can
like the text in a div might be dynamic but the div doesnt have to be and its parent doesnt need to be either
your option is to await with out suspense(dynamic option which isnt possible) and you will see a white screen until untill data resolves or you can use the static shell and fall backs to get as much to the screen as you can'
becuase thats what you want to do is make the route dynamic so you can await with out showing a fall back which will result in showing white on the screen untill the data resolves what you asking for wont do what you think it will
dynamic pages with out suspense stop rendering at await its a blocker so you will see white
Giant pandaOP
In your invoices example, you have static content at the page level: the "Invoices" heading, the description, the "Create Invoice" button. Those go in the static shell, and only the table is the dynamic hole. That's the PPR pattern, I get it.
My case is different. Every visible element on the page depends on auth state and search params. There's no equivalent heading or button to extract. If I follow your pattern, the page becomes:
Awaiting without Suspense is a valid pattern when cache components are disabled. Without cache component enabled, it would not show a blank page. The user would not see anything change visually until the page is ready to render and then the navigation would happen. Already deployed and tested this with cache components disabled.
My case is different. Every visible element on the page depends on auth state and search params. There's no equivalent heading or button to extract. If I follow your pattern, the page becomes:
export default function MyPage() {
return (
<Suspense>
<EntirePageContent />
</Suspense>
);
}Awaiting without Suspense is a valid pattern when cache components are disabled. Without cache component enabled, it would not show a blank page. The user would not see anything change visually until the page is ready to render and then the navigation would happen. Already deployed and tested this with cache components disabled.
plz show the content to the page
i need to see that component
it needs to be broken up to better work with cache components if you wan tto use it with out should a huge fallback
as i said even if you could make the route dynamic you would show a white screen or delayed nav if you were able to
using await stoped the render with out suspense so the html part (ui part) never gets rendered it stoped at the js at await
it simulates a long blocker no fall back ( as best you can with cache components)
awaiting with out suspense causes the ui not to load until the data is resolved
in the demo there is has a delay so you can see what a slow auth or api data fetch would seem like
it allows you to see the issue with awaiting with out suspense and even tho you can do that with out cache compoents you really wouldnt want to
as far as I know its not possible to do what you want and, if you could you wouldnt want to
if you show me the compoent tho i would help breake it up for you so it can work with cache components
Giant pandaOP
I think okay with the delayed navigation for this route since the dynamic data is just search params and auth state. I do use the ppr pattern you describe in other routes.
Okay let me describe the page a bit more. I'll include some of the code as well. Most of the content on this page is based on the current selected tab. And the state of the tab selected is based on the search params. So effectively the entire page is a client component. I could extract parts like the heading + description to a static shell but then I would have to show some fallback loading ui for basically the entirety of the rest of the page. I would ideally only show loading ui for the dynamic "holes" in the ui that need it but the entire ui is conditionally rendered based on the tab selected.
So I would effectively have to wrap the majority of the page in a Suspense since it relies on search params.
Okay let me describe the page a bit more. I'll include some of the code as well. Most of the content on this page is based on the current selected tab. And the state of the tab selected is based on the search params. So effectively the entire page is a client component. I could extract parts like the heading + description to a static shell but then I would have to show some fallback loading ui for basically the entirety of the rest of the page. I would ideally only show loading ui for the dynamic "holes" in the ui that need it but the entire ui is conditionally rendered based on the tab selected.
So I would effectively have to wrap the majority of the page in a Suspense since it relies on search params.
@Patrick MacDonald as far as I know its not possible to do what you want and, if you could you wouldnt want to
Giant pandaOP
That's exactly what this github issue is about: https://github.com/vercel/next.js/issues/86739
Cache components is almost like a lifestyle choice if you will LOL
It gives you great caching abilities across deployments which is amazing and it also allows you to serve a static shell from a CDN.
But it's designed to not allow blocking during rendering and to optimize as much to the Shell as possible
In my opinion, your two choices are write a really good fallback and just leave out a spot for all the dynamic data or come over to the dark side and rewrite the component to work better with cache components
The whole point of the static shell is that your header your navbar your footer. Everything on the website gets to the page right away
It's vercels ssr's answer to slow navigations
Server as much from a cdn
Giant pandaOP
Yeah I like using cache components. But this specific page is a bit more tricky, especially since it's effectively the home page. I'd rather not show loading skeleton on the home page.
// page.tsx (server)
import { loadPageSearchParams } from "@/lib/search-params.server";
export default async function Page({ searchParams }) {
await loadPageSearchParams(searchParams);
return <PageContent />;
}// page-content.tsx (client)
"use client";
import { useQueryState } from "nuqs";
export function PageContent() {
const [tab] = useQueryState("tab", tabParser); // "a" | "b"
const [filter] = useQueryState("filter", filterParser);
const [query] = useQueryState("q", queryParser);
const { limits } = useUserPlan(); // auth-dependent
return (
<>
{/* Hero — collapses based on tab and query state */}
<section className={cn(tab === "b" && query ? "h-0 opacity-0" : "")}>
<h1>Heading</h1>
<p>Description</p>
</section>
<Tabs value={tab}>
<TabsList>
<TabsTrigger value="a">Tab A</TabsTrigger>
<TabsTrigger value="b">Tab B</TabsTrigger>
</TabsList>
<TabsContent value="a">
<SearchView query={query} />
</TabsContent>
<TabsContent value="b">
<FeatureGatedInput enabled={limits?.featureX} />
<FilterSelector selected={filter} />
<Results filter={filter} />
</TabsContent>
</Tabs>
<BottomBar />
</>
);
}basic gist of the current page
It's like you're a vegetarian but somebody has given you a pizza recipe with meat. That looks really good
Giant pandaOP
I totally get everything you are saying lol.
I'm now starting to understand what your saying
I think for this one you just show a really good fallback which is annoying because you basically have to have double the code in two places
Giant pandaOP
Yeah that was something I considered earlier but felt icky lol
It is and it isn't. If you can convince yourself that it's a good thing then it's kind of cool if you really think about it
Lol
Think about it because the way react updates the Dom and the virtual Dom if your fallback content is exactly the same as the content that's going to be there
Then it doesn't change and it's more like preloading part of the UI then showing a fallback per se
The only other way is to ditch the search params for your tabs switching
And just use state
Do you know what I mean? There's a lot more of that component that can be part of the static shell if the tabs don't need to be controlled with the URL
Giant pandaOP
But then also, certain parts of the ui collapses so depending on the selected tab. So to have an accurate fallback, I would basically have to make a fallback copy for each variant of state.
I was using use state before this but since this page ui is pretty dynamic, it benefits a lot from being able to persist the specific ui layout and selections in the url.
I was using use state before this but since this page ui is pretty dynamic, it benefits a lot from being able to persist the specific ui layout and selections in the url.
I'm working on something that mmight work
import { Suspense, useState } from "react";
export default function page(props: PageProps<"/">) {
const queryPromise = props.searchParams.then((params) =>
Array.isArray(params.query) ? params.query[0] : String(params.query),
);
const limitsPromise = fetchLimits();
const filterPromise = props.searchParams.then((params) =>
Array.isArray(params.filter) ? params.filter[0] : String(params.filter),
);
return <TabsContent queryPromise={queryPromise} limitsPromise={limitsPromise} filterPromise={filterPromise} />;
}
/////--------- different file below so it can be a client
'use client'
export function TabsContent({
queryPromise,
limitsPromise,
filterPromise,
}: any) {
const [tab, setTab] = useState<"a" | "b">("a");
return (
<>
{/* Hero — collapses based on tab and query state */}
<section className={cn(tab === "b" && query ? "h-0 opacity-0" : "")}>
<h1>Heading</h1>
<p>Description</p>
</section>
<Tabs value={tab}>
<TabsList>
<TabsTrigger value="a">Tab A</TabsTrigger>
<TabsTrigger value="b">Tab B</TabsTrigger>
</TabsList>
<TabsContent value="a">
<Suspense>
<SearchView queryPromise={queryPromise} />
</Suspense>
</TabsContent>
<TabsContent value="b">
<Suspense>
<FeatureGatedInput enabled={limitsPromise} />
</Suspense>
<Suspense>
<FilterSelector selectedPromise={filterPromise} />
<Results filter={filterPromise} />
</Suspense>
</TabsContent>
</Tabs>
<BottomBar />
</>
);
}@Giant panda I'm thinking something like this, we get the params from the server on the page and use .then() so we can start the promise but not await our use use() untill the child
youd still have to figure out the ui switch tho in that css
then still write good fall backs of corse too
smaller ones
Giant pandaOP
Oh so only the tab would use useState and the rest would use search params. And then show fallbacks for the ui under each tab.
and if the child components are client you can use use() with prmosies passed in this way
the search params would be coming from the server this way and we can show way more of the ui
idk just trying to think of ways to get the most out of this while still using cache components
lol think of the diagram !!!!!! lol
Giant pandaOP
Yeah that could work well actually.
I originally didn't want to show any fallbacks since this page currently works as the home page but I guess this would be a smaller fallback.
I originally didn't want to show any fallbacks since this page currently works as the home page but I guess this would be a smaller fallback.
I'll give that a try lol
and you would load all the content around it header footer nav
ppr is about more than the page its about the layout and ui too
lol I wouldnt mind if you marked sovled or correct but also if you wanted to try ti and come back thats cool to
Giant pandaOP
I'll mark it as solved lol. We kinda explored all the possible solutions I think.
Im happy we could find something that fits the use case
obv in my example i used any but thats not how id do it
it was just to get the code out lol
if you dont want to get the search params from the server you can use use you parser or use search params in the child components too
as long as they are wrapped in suspense shoudlnt be an issue
Giant pandaOP
Yeah got it. I'll give it a try.
@Giant panda you can opt out of sending the static shell and use await in 16.3
" To make a navigation instant, you "turn" an asynchronous operation into something that can be available instantly:
Stream with <Suspense>. The user will instantly see a loading state (with more UI streaming in after).
Cache with 'use cache'. The user will instantly see a previously cached UI (reused between requests).
In both cases above, the navigation will feel SPA-like and instant to the user.
However, sometimes you might want to make a certain navigation server-bound. For example, a blog might choose to never show a loading shell for posts. For those cases, you can tell Next.js that you want this navigation to Block:
// in page.tsx or layout.jsx
export const instant = false;
"
https://nextjs.org/blog/next-16-3-instant-navigations#weve-been-using-it-ourselves
" To make a navigation instant, you "turn" an asynchronous operation into something that can be available instantly:
Stream with <Suspense>. The user will instantly see a loading state (with more UI streaming in after).
Cache with 'use cache'. The user will instantly see a previously cached UI (reused between requests).
In both cases above, the navigation will feel SPA-like and instant to the user.
However, sometimes you might want to make a certain navigation server-bound. For example, a blog might choose to never show a loading shell for posts. For those cases, you can tell Next.js that you want this navigation to Block:
// in page.tsx or layout.jsx
export const instant = false;
"
https://nextjs.org/blog/next-16-3-instant-navigations#weve-been-using-it-ourselves
From my understanding, if you use this you can await on the page without the error that cache components causes
Not advocating for this. LOL but it's an option
Giant pandaOP
Yeah it might work using the
export const instant=false but I found the blocking experience to be too slow. But honestly the issue I had with all that was the use of useSearchParams on the page. No way to defer the reading of search params to be purely on the client which would be my ideal solution. But would require a useEffect and rolling my own way to read and update url state. So the solution I landed on was wrapping the dynamic portion of the page in a suspense and then using a static clone of it as the fallback. Not ideal but the best option I found.My friend
@Giant panda
You can use them on the page have you page not async and the component that uses them is dynamic
You use .then() so you don't have to await the search params on the page and you can await them on the server or use use() on the client while using suspense
If your interested I can show the code
I can get your search params to a client component while still having your page be part of the static shell
Giant pandaOP
But that still requires wrapping the dynamic part in a suspense and using a fallback, which is what I'm doing now, right?
What I mean by search params being deferred to being purely on the client, I mean the page can be rendered statically in it's default state and then on the client, read search params and update the ui based on that state.
What I mean by search params being deferred to being purely on the client, I mean the page can be rendered statically in it's default state and then on the client, read search params and update the ui based on that state.
I think at this point then we're just arguing about definition
You just have to make your fallback the default state
And when the data resolves it either shows the same default state or a new state
When someone navigates to the page, the fallback will be the default State and part of the statically rendered shell
Fallbacks are part of the static shell
Giant pandaOP
Yes that's exactly what I'm doing now. It's the best solution I have found but also not perfect since when visiting url with search params, it will flash the default state and then swap basically the entire page to the "real" one. Just not ideal. Ideally, I would be able to avoid even having a static shell and duplicating the page by being able to render the page using the default state and once the page mounts on the client, read the url search params and swap states. I know it's not a huge difference but duplicating the dynamic portion of the page and using it as a fallback it's just too hacky for my liking. Do you understand what I mean?
The only part that should flash would be the content that is different and I mean I think there's no shame in using a fall back to get UI to the screen, it's just good design if it doesn't have to change and hey could always make a component wrapper for and use it in the fallback and "real component"
Giant pandaOP
Hopefully this demonstrates what I'm saying well. The first half of the video, I am reloading the page with a search param in the url. It flashes the default state and then swap to correct state based on the search params. But ideally, that transition would be like what I show in the 2nd half of the video where I click to manually switch between states. Like if I was able to defer the reading of the search params to the client mounting, the transition would be exactly like that. It would show the default state and transition to the "repo" tab like shown in the video. Does that make sense?
There's something very strange going on with those random flashes at the start of the video. That's not ideal at all
Is the project open source I would be more than happy to take a look at that tomorrow
Giant pandaOP
That's me refreshing the page. It flashes the default state.
Sorry I know we talked about all this originally but it's been so long LOL
Giant pandaOP
Yeah lol all good
So the default state and the real state are very very different
Giant pandaOP
I can dm you the link so you can test it out on your end and understand better?
Sure I'm drinking tonight so I'm not sure how much I'll get too tonight but tomorrow I'm your guy lol
Giant pandaOP
lol yeah no problem.
Hey what if you didn't show a fallback?
Then if there's a search param it resolves that way and if there isn't a search params it resolves the other way, like use suspense without a fallback, could that work?
Then it would just look like the page took a second to load
Giant pandaOP
Suspense without a fallback will then flash a blank ui which I would argue is worse.
Well dm me the repo I'll look at her tomorrow