Next.js Discord

Discord Forum

Cache Components: Is there per route ppr?

Unanswered
Giant panda posted this in #help-forum
Open in Discord
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.

128 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.
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>
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 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.
lol
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:


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.
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.
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'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.
yes
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'll give that a try lol
yes
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.
rad