cache components -> stale router cache
Unanswered
Yellow croaker posted this in #help-forum
Yellow croakerOP
Hey. So has anyone used cache components?
I'm running into the following issue:
I'm using wordpress as the cms / backend for a project and wpgraphql for queries.
I have a revalidation api endpoint that im using as webhook on the server for revalidation
where i do on demand revalidateTag for a specific cacheTag. I'm using expire 0 as the second param which i thought would be the only thing needed. I also do revalidatePath("/", "layout") there
Then when I build and start. New cms content, refresh. Shows up. -> I navigate to another page -> i click my logo to go home (or browser back nav) -> Stale data (state before latest cms change).
Even subsequent navigations don't solve this. A second refresh IS ALWAYS needed for no stale data anywhere.
Has anyone had a similar experience?
Thing of note. I'm not using updateTag as that can't be used in Route Hanlers.
I'm running into the following issue:
I'm using wordpress as the cms / backend for a project and wpgraphql for queries.
I have a revalidation api endpoint that im using as webhook on the server for revalidation
where i do on demand revalidateTag for a specific cacheTag. I'm using expire 0 as the second param which i thought would be the only thing needed. I also do revalidatePath("/", "layout") there
Then when I build and start. New cms content, refresh. Shows up. -> I navigate to another page -> i click my logo to go home (or browser back nav) -> Stale data (state before latest cms change).
Even subsequent navigations don't solve this. A second refresh IS ALWAYS needed for no stale data anywhere.
Has anyone had a similar experience?
Thing of note. I'm not using updateTag as that can't be used in Route Hanlers.
13 Replies
Have you tired cacheLife and trying setting stale revalidate and expire all to 0? @Yellow croaker
Or cacheLife("seconds") seems to have a good profile for up-to-date data
Yellow croakerOP
@Patrick MacDonald the issue was more that my '/' route was static because everything was resolvable at build time. This forced SWR scemantics, even though my main point is that I use a route handler for revalidation of tags with expire at 0, and an sse broker that broadcasts updates to connected clients with a router.refresh() on broadcast. That's supposed to clear router cache (i think) but with static homepage it didn't. The solution in my case was to use await connection() (from https://nextjs.org/docs/app/api-reference/functions/connection) . This forces page to be PPR. I don't know if that's the best pattern here, but it seems to fix my issue, and allows me to have cacheLife of hours as a fallback if the webhook fails or something.
If the home page is static, why do we need to revalidate it?
It sounds like the data is being fetched on the client making the page static. We need a way of making the hook run again. Revalidate path and revalidate tag and update tag are for refetching data from the server. If you're fetching it from the client, you need to invalidate it on the client somehow or a way of unmounting and remounting the client component. That's fetching the data
And ISR but if the page is static there is nothing to ISR because the page is the same every revalidate
If you want to use revalidate, you'll have to fetch the data on the server side and pass it to the client
@Yellow croaker or you could use something like tanstack useQuery to invalidate on the client
Yellow croakerOP
Hey, thanks for the suggestions! Just to clarify, the data isn't fetched on the client at all. Here's how it works:
I have async Server Components that call data functions with "use cache" + cacheTag("events") + cacheLife("hours"). These run entirely on the server. The GraphQL call to WordPress happens server-side, and the result is cached in the server's function cache. The client component (event list) just receives the data as a prop, it never fetches anything itself.
For revalidation I have two steps:
1. WordPress webhook hits a Route Handler that calls revalidateTag("events", { expire: 0 }) This force-expires the server function cache
2. An SSE broker broadcasts to connected browsers, and the client calls router.refresh()
This clears the Router Cache so the browser requests fresh server-rendered content.
The stale data issue I had was because my homepage had no dynamic APIs, so Next.js treated it as a fully static route. Static routes use the Full Route Cache which has SWR semantics. It serves the stale pre-built HTML and revalidates in the background. So even though my function cache was correctly expired by the webhook, the Full Route Cache sat in front of it and kept serving the old page until a second request came in.
await connection() inside a Suspense boundary fixed it because it opts the route into PPR. Now the static shell is served instantly, but the dynamic hole (where the data lives) is reexecuted on every request. That request hits the function cache. If it's been expired by the webhook, it fetches fresh data from WordPress. If not, it serves the cached result. So I get instant page loads + fresh data within seconds of a CMS change, without any polling or client-side fetching.
It's the first time I'm using this paradigm, but I think it works. If my logic has gaps and you'd like to add anything or correct me, I'd be happy to learn
I have async Server Components that call data functions with "use cache" + cacheTag("events") + cacheLife("hours"). These run entirely on the server. The GraphQL call to WordPress happens server-side, and the result is cached in the server's function cache. The client component (event list) just receives the data as a prop, it never fetches anything itself.
For revalidation I have two steps:
1. WordPress webhook hits a Route Handler that calls revalidateTag("events", { expire: 0 }) This force-expires the server function cache
2. An SSE broker broadcasts to connected browsers, and the client calls router.refresh()
This clears the Router Cache so the browser requests fresh server-rendered content.
The stale data issue I had was because my homepage had no dynamic APIs, so Next.js treated it as a fully static route. Static routes use the Full Route Cache which has SWR semantics. It serves the stale pre-built HTML and revalidates in the background. So even though my function cache was correctly expired by the webhook, the Full Route Cache sat in front of it and kept serving the old page until a second request came in.
await connection() inside a Suspense boundary fixed it because it opts the route into PPR. Now the static shell is served instantly, but the dynamic hole (where the data lives) is reexecuted on every request. That request hits the function cache. If it's been expired by the webhook, it fetches fresh data from WordPress. If not, it serves the cached result. So I get instant page loads + fresh data within seconds of a CMS change, without any polling or client-side fetching.
It's the first time I'm using this paradigm, but I think it works. If my logic has gaps and you'd like to add anything or correct me, I'd be happy to learn
When you tired realdiatePath did you try ("/", "page") instead of layout
I use updateTag alot with great success but thats with actions and you need it for a route
Yellow croakerOP
No I did ("/", "layout") . The only thing is, I understand this always nuked the entire page cache, and caused a full refetch on everything. I though of this as wasteful. That's why I wanted to move to tags, hence the switch to PPR. Current method seems to work to be fair. Though I think the whole system is more so meant for use with 'server actions' than external data through route handlers, with webhooks etc
You're correct, there is more granular control with tags