cacheTag without without 'use cache'
Unanswered
Blue orchard bee posted this in #help-forum
Blue orchard beeOP
This is more of a feedback point than anything. I am super excited about the possibilities of the new composition of the cacheComponents but a bit bummed out by the 'dynamic by default' consequences of them. Most of my content comes from Sanity CMS and it has an amazing feature called syncTags which every query can return. These syncTags are a representation of the content that fetch is getting and are therefore the perfect tags for something like cacheTag(). The fact the new composition allows setting the cacheTag after the results of a fetch are made also means I can directly add then into that function next to the fetch.
Currently, I have been using the unstable_cache wrapper, placing a scoped but static cacheTag then saving it against the syncTags to redis inside the function which is being cached. This works really nicely and means when a change happens in my CMS it emits the syncTags(s) and I pick those up and call revalidateTag in an endpoint. This in turn means the full route cache loses one of it's dependancies and therefore is cleared. So the next visitor gets SWR behaviour after a change. It works incredibly well and I thought the introduction of cacheTag() as part of the cacheComponents would also allow me to drop the whole redis step. But it seems that's not the case.
Now, because cacheComponents make my routes force dynamic I either have to accept all my routes are dynamic now unnecessarily or somehow push my syncTags up to the route level cacheTag() to invalidate the page as a cacheComponent. Alternatively I have to somehow related my syncTags with the path they are on and call revalidatePath() but that also feels messy and prop drilly. Am I missing something here (I hope I am) or is it actually better for me to stick with my current approach and not use cacheComponents?
What I would love is the ability to use the cacheTag() functionality with the unstable_cache pattern and no force dynamic opt in. I'm sure there's a good reason I can't but yeah :/
Currently, I have been using the unstable_cache wrapper, placing a scoped but static cacheTag then saving it against the syncTags to redis inside the function which is being cached. This works really nicely and means when a change happens in my CMS it emits the syncTags(s) and I pick those up and call revalidateTag in an endpoint. This in turn means the full route cache loses one of it's dependancies and therefore is cleared. So the next visitor gets SWR behaviour after a change. It works incredibly well and I thought the introduction of cacheTag() as part of the cacheComponents would also allow me to drop the whole redis step. But it seems that's not the case.
Now, because cacheComponents make my routes force dynamic I either have to accept all my routes are dynamic now unnecessarily or somehow push my syncTags up to the route level cacheTag() to invalidate the page as a cacheComponent. Alternatively I have to somehow related my syncTags with the path they are on and call revalidatePath() but that also feels messy and prop drilly. Am I missing something here (I hope I am) or is it actually better for me to stick with my current approach and not use cacheComponents?
What I would love is the ability to use the cacheTag() functionality with the unstable_cache pattern and no force dynamic opt in. I'm sure there's a good reason I can't but yeah :/
79 Replies
Black-bellied Plover
Yup, the fetch includes a GROQ query, kinda like graphQL, I should mention these ‘fetches’ are using the sanity client.
@Black-bellied Plover Yup, the fetch includes a GROQ query, kinda like graphQL, I should mention these ‘fetches’ are using the sanity client.
how did you manage to do it with unstable_cache? i still dont get the picture sorry
Blue orchard beeOP
const getCartCached = (cacheTag: string) =>
unstable_cache(async (locale: SupportedLocale) => getCartContent({ locale, cacheTag }), ['getCart'], {
tags: [cacheTag],
})
Here cacheTag would be the static tag like ''getCartContent:en" then inside the getCartContent function I make the fetch, get the result, pull out the syncTags, save them against the cacheTag in redis, return the result.
unstable_cache(async (locale: SupportedLocale) => getCartContent({ locale, cacheTag }), ['getCart'], {
tags: [cacheTag],
})
Here cacheTag would be the static tag like ''getCartContent:en" then inside the getCartContent function I make the fetch, get the result, pull out the syncTags, save them against the cacheTag in redis, return the result.
So this example would be getting content for the cart... like translations such as "Add to cart" not the products of course
im not quite sure i understand the architecture. can the syncTag of getCartContent differ?
wouldn't it supposed to be:
?
const getCartCached = (locale: SupportedLocale) =>
unstable_cache(async (locale: SupportedLocale) => getCartContent({ locale, cacheTag }), ['getCart'], {
tags: [`getCartContent:${locale}`],
})(locale)?
Blue orchard beeOP
Your example is equivalent to mine:
Well they vary based on the locale and based on the content which is in the CMS and when it changes. So if the query gets 4 fields then each field will be represented by a syncTag, when that content is changed the CMS emits that syncTag as being changed. I can then match that up with the static cacheTag in redis and revalidateTag in an endpoint. My hope was, with cacheTag() I could simply return the syncTags from the getCartContent() and place them directly as the tags:
Thus eliminating the need for the redis step to pair up the dynamic syncTags with the static cacheTag
const getCartCached = (cacheTag: string) =>
unstable_cache(async (locale: SupportedLocale) => getCartContent({ locale, cacheTag }), ['getCart'], {
tags: [cacheTag],
})
const cart = getCartCached(`getCart:${locale}`)(locale),Well they vary based on the locale and based on the content which is in the CMS and when it changes. So if the query gets 4 fields then each field will be represented by a syncTag, when that content is changed the CMS emits that syncTag as being changed. I can then match that up with the static cacheTag in redis and revalidateTag in an endpoint. My hope was, with cacheTag() I could simply return the syncTags from the getCartContent() and place them directly as the tags:
const getCartCached = async (cacheTag: string) => {
'use cache'
const {data, syncTags} = getCartContent({ locale })
cacheTag([...syncTags])
return data
}Thus eliminating the need for the redis step to pair up the dynamic syncTags with the static cacheTag
getCartContent:${locale}. But doing so opts me into dynamic by default and therefore I either have to accept my pages are dynamic when they need not be or I have to pass up those syncTags to the page level to add them to the page level cacheTag() so i can get the page to revalidate when its child cacheComponent revalidates. Before, revalidating the data cache of the unstable_cache would mean the full route cache would need to refetch because the data it depends on would have been revalidated.im still quite not sure about your architecture... can synctags change in the same build or are they set and fixed in the same build? would there be a chance of creating synctags during runtime?
Blue orchard beeOP
If I add 'use cache' to the page then it will be it's own cache entry and won't be revalidated when the nested function-based cache is. (If I'm wrong about this it would be amazing... a cascading revalidation would solve all of this).
Yes, syncTags are constantly changing as content is changing. When the content they represent changes they are emitted and the associated tag(s) are revalidated. So they are not really connected to builds but are used for data cache revalidation only.
Here is an example repo from Sanity which might help:
The long listener which picks up on the syncTags emitted from the CMS when content changes in realtime: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/expirator-service/index.ts
The api endpoint which is called to revalidate those tags: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/src/app/api/expire-tags/route.ts
The fetcher which is a 'use cache' component: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/src/sanity/fetch.ts
The issue I see here is that the homepage is marked as 'use cache' too but I don't understand how that is revalidated: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/src/app/page.tsx
Maybe mu assumption is wrong and the revalidation of a cacheComponent causes a cascade upwards of revalidation of the parent cacheComponents... if that is the case I am very happy... I will test this out
Yes, syncTags are constantly changing as content is changing. When the content they represent changes they are emitted and the associated tag(s) are revalidated. So they are not really connected to builds but are used for data cache revalidation only.
Here is an example repo from Sanity which might help:
The long listener which picks up on the syncTags emitted from the CMS when content changes in realtime: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/expirator-service/index.ts
The api endpoint which is called to revalidate those tags: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/src/app/api/expire-tags/route.ts
The fetcher which is a 'use cache' component: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/src/sanity/fetch.ts
The issue I see here is that the homepage is marked as 'use cache' too but I don't understand how that is revalidated: https://github.com/sanity-io/lcapi-examples/blob/main/next-enterprise/src/app/page.tsx
Maybe mu assumption is wrong and the revalidation of a cacheComponent causes a cascade upwards of revalidation of the parent cacheComponents... if that is the case I am very happy... I will test this out
>If I add 'use cache' to the page then it will be it's own cache entry and won't be revalidated when the nested function-based cache is. (If I'm wrong about this it would be amazing... a cascading revalidation would solve all of this).
as far as im concerned this is how cache works in pre-cacheComponent. if you used unstable_cache or fetch in a static page, then if you call revalidateTag it would also statically RE-render the static route.
as far as im concerned this is how cache works in pre-cacheComponent. if you used unstable_cache or fetch in a static page, then if you call revalidateTag it would also statically RE-render the static route.
Blue orchard beeOP
Exactly, but this doesn't seem to be how it works now with cacheComponents: true which is the whole issue... to make the page no dynamic I have to mark it as use cache but then I need to revalidate the page as well as its child function... at least that's what it seems like
really?
ill try to reproduce
Blue orchard beeOP
I made a super small app and it looks like I'm wrong and it does indeed 'bubble up' the revalidation. Please forgive my love for arrow functions haha:
Clicking the button does indeed update the time.
import revalidate from "@/actions/revalidate";
import { cacheTag } from "next/cache";
const TimeComponent = async () => {
"use cache";
cacheTag("time");
const time = new Date().toLocaleTimeString();
return <p>The current time is: {time}</p>;
};
const RevalidateButton = async () => {
return (
<button
className="bg-blue-400 text-white font-semibold px-4 py-2"
onClick={revalidate}
>
Revalidate
</button>
);
};
const Home = async () => {
"use cache";
return (
<div className="flex flex-col items-center justify-center h-screen w-screen gap-4">
<TimeComponent />
<RevalidateButton />
</div>
);
};
export default Home;
///
"use server";
import { updateTag } from "next/cache";
export default async function revalidate() {
updateTag("time");
}Clicking the button does indeed update the time.
Blue orchard beeOP
What is very interesting though is when I add another cacheComponent called AltTimeComponent, locally clicking the revalidate button only revalidates the TimeComponent as expected. But when I build and deploy it both revalidate
import revalidate from "@/actions/revalidate";
import { cacheLife, cacheTag } from "next/cache";
const TimeComponent = async () => {
"use cache";
cacheTag("time");
cacheLife("max");
const time = new Date().toLocaleTimeString();
return <p>The current time is: {time}</p>;
};
const AltTimeComponent = async () => {
"use cache";
cacheTag("alt-time");
cacheLife("max");
const time = new Date().toLocaleTimeString();
return <p>The current time is: {time}</p>;
};
const RevalidateButton = async () => {
return (
<button
className="bg-blue-400 text-white font-semibold px-4 py-2"
onClick={revalidate}
>
Revalidate
</button>
);
};
const Home = async () => {
"use cache";
cacheLife("max");
return (
<div className="flex flex-col items-center justify-center h-screen w-screen gap-4">
<p>Alternative Time Component:</p>
<AltTimeComponent />
<p>Time Component inside Useless Wrappers:</p>
<TimeComponent />
<RevalidateButton />
</div>
);
};
export default Home;It also updates the time on refresh 

ohhh thanks for reproducing it for me, sorry i was busy for a while
since we already have that since pre-cacheComponent
the component caching one is the one thats new and might be.... umm idk weird (?)
also please add a language to the snippet like so:
```tsx
paste code here
```
```tsx
paste code here
```
Blue orchard beeOP
Okay I removed everything but the cacheTag() s and put the use cache to the top of the file... still clicking the revalidate button changes both times
interesting
Blue orchard beeOP
Ah of course its because they are equating to the same value... interesting that the cacheTag does not contribute towards that... if I swap them out for unstable_cache with an dampty array for the keyParts I get the same result... if I add 'time' and 'alt-time' respectively I indeed can get only one to change at a time
This is a little bit of lost functionality/specificity I suppose
Oh no but using a different locale for the alt-time also results in both changing when using cacheComponents!
im lost 😅
here, it works with 2 independent cache keys and clicking revalidate/update will rerender the whole page while keeping the cached component the same.
this is on build
its because they are equating to the same value...i dont get it. wdym?
interesting that the cacheTag does not contribute towards that...towards what?
if I swap them out for unstable_cache with an dampty array for the keyParts I get the same result...swap what out?
This is a little bit of lost functionality/specificity I supposeits been like this pre-cachedComponent...
Oh no but using a different locale for the alt-time also results in both changing when using cacheComponents!what?
Blue orchard beeOP
My code example is too large so will send it in two messages
"use cache";
import revalidate from "@/actions/revalidate";
import revalidateAlt from "@/actions/revalidateAlt";
import { cacheTag, unstable_cache } from "next/cache";
const cachedTime = unstable_cache(
async () => new Date().toLocaleTimeString(),
["time"],
{ tags: ["time"] }
);
const UnstableCacheTimeComponent = async () => {
const time = cachedTime();
return <p>The current time is: {time}</p>;
};
const cachedAltTime = unstable_cache(
async () => new Date().toLocaleTimeString("fr-FR"),
["alt-time"],
{ tags: ["alt-time"] }
);
const UnstableCacheAltTimeComponent = () => {
const time = cachedAltTime();
return <p>The alt current time is: {time}</p>;
};
const TimeComponent = () => {
cacheTag("time");
const time = new Date().toLocaleTimeString();
return <p>The current time is: {time}</p>;
};
const AltTimeComponent = () => {
cacheTag("alt-time");
const time = new Date().toLocaleTimeString("fr-FR");
return <p>The alt current time is: {time}</p>;
};
const RevalidateButton = async () => {
return (
<button
className="bg-blue-400 text-white font-semibold px-4 py-2"
onClick={revalidate}
>
Revalidate
</button>
);
};
const RevalidateAltButton = async () => {
return (
<button
className="bg-blue-400 text-white font-semibold px-4 py-2"
onClick={revalidateAlt}
>
Revalidate Alt
</button>
);
};
const Card = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={`p-3 flex flex-col items-center gap-2 border border-blue-200 rounded-md ${className}`}
>
{children}
</div>
);
};const Home = async () => {
return (
<div className="flex items-center justify-center h-screen w-screen gap-4">
<Card className="flex flex-col items-center gap-4">
<h1 className="font-bold">cacheComponents</h1>
<Card>
<p>Alternative Time Component:</p>
<AltTimeComponent />
<RevalidateAltButton />
</Card>
<Card>
<p>Time Component:</p>
<TimeComponent />
<RevalidateButton />
</Card>
</Card>
<Card className="flex flex-col items-center gap-4">
<h1 className="font-bold">unstable_cache</h1>
<Card>
<p>Alternative Time Component:</p>
<UnstableCacheAltTimeComponent />
<RevalidateAltButton />
</Card>
<Card>
<p>Time Component:</p>
<UnstableCacheTimeComponent />
<RevalidateButton />
</Card>
</Card>
</div>
);
};
export default Home;please add
tsx after the triple backtick.and please send minimal reproduction code.
this is making it harder to discuss
Blue orchard beeOP
Apologies I'm new to using discord
testBlue orchard beeOP
Thanks
Blue orchard beeOP
"use cache";
import revalidate from "@/actions/revalidate";
import revalidateAlt from "@/actions/revalidateAlt";
import { cacheTag, unstable_cache } from "next/cache";
const cachedTime = unstable_cache(
async () => new Date().toLocaleTimeString(),
["time"],
{ tags: ["time"] }
);
const UnstableCacheTimeComponent = async () => {
const time = cachedTime();
return <p>The current time is: {time}</p>;
};
const cachedAltTime = unstable_cache(
async () => new Date().toLocaleTimeString("fr-FR"),
["alt-time"],
{ tags: ["alt-time"] }
);
const UnstableCacheAltTimeComponent = () => {
const time = cachedAltTime();
return <p>The alt current time is: {time}</p>;
};
const TimeComponent = () => {
cacheTag("time");
const time = new Date().toLocaleTimeString();
return <p>The current time is: {time}</p>;
};
const AltTimeComponent = () => {
cacheTag("alt-time");
const time = new Date().toLocaleTimeString("fr-FR");
return <p>The alt current time is: {time}</p>;
};
const RevalidateButton = async () => {
return (
<button
className="bg-blue-400 text-white font-semibold px-4 py-2"
onClick={revalidate}
>
Revalidate
</button>
);
};
const RevalidateAltButton = async () => {
return (
<button
className="bg-blue-400 text-white font-semibold px-4 py-2"
onClick={revalidateAlt}
>
Revalidate Alt
</button>
);
};
const Home = async () => {
return (
<div>
<h1 className="font-bold">cacheComponents</h1>
<p>Alternative Time Component:</p>
<AltTimeComponent />
<RevalidateAltButton />
<p>Time Component:</p>
<TimeComponent />
<RevalidateButton />
<h1 className="font-bold">unstable_cache</h1>
<p>Alternative Time Component:</p>
<UnstableCacheAltTimeComponent />
<RevalidateAltButton />
<p>Time Component:</p>
<UnstableCacheTimeComponent />
<RevalidateButton />
</div>
);
};
export default Home;thanks
so what is the issue?
"use cache"
import { cacheTag, unstable_cache } from "next/cache"
import { revalidate, revalidateAlt } from "./action"
const cachedTime = unstable_cache(
async () => new Date().toLocaleTimeString(),
["time"],
{ tags: ["time"] }
)
const UnstableCacheTimeComponent = async () => {
const time = cachedTime()
return <>The current time is: {time}</>
}
const cachedAltTime = unstable_cache(
async () => new Date().toLocaleTimeString("fr-FR"),
["alt-time"],
{ tags: ["alt-time"] }
)
const UnstableCacheAltTimeComponent = () => {
const time = cachedAltTime()
return <>The alt current time is: {time}</>
}
const TimeComponent = async () => {
"use cache"
cacheTag("time")
const time = new Date().toLocaleTimeString()
return <>The current time is: {time}</>
}
const AltTimeComponent = async () => {
"use cache"
cacheTag("alt-time")
const time = new Date().toLocaleTimeString("fr-FR")
return <>The alt current time is: {time}</>
}
const RevalidateButton = async () => {
return (
<button onClick={revalidate}>
Revalidate
</button>
)
}
const RevalidateAltButton = async () => {
return (
<button onClick={revalidateAlt}>
Revalidate Alt
</button>
)
}
const Home = async () => {
return (
<>
<p>Alternative Time Component:</p>
<AltTimeComponent />
<RevalidateAltButton />
<p>Time Component:</p>
<TimeComponent />
<RevalidateButton />
<p>Alternative Time Component:</p>
<UnstableCacheAltTimeComponent />
<RevalidateAltButton />
<p>Time Component:</p>
<UnstableCacheTimeComponent />
<RevalidateButton />
</>
)
}
export default Homethis should work as you wanted.
Blue orchard beeOP
Well the AltTimeComponent revalidates when RevalidateButton is clicked... which is shouldn't be
@Blue orchard bee Well the AltTimeComponent revalidates when RevalidateButton is clicked... which is shouldn't be
have you tried with the code i gave you?
altTimeComponent doesn't revalidate when the revalidatebutton is clicked
Blue orchard beeOP
Ah two seconds let me check
So you moved the use cache inside the components?
no i added the use cache inside the components.
Blue orchard beeOP
I see... so the 'use cache' at the top of a file marks only the default export as a cacheComponent?
almost correct
the 'use cache' at the top of a file marks the route itself as a static route
thereby making a cache boundary for the whole route
cache component are these:
you put "use cache" in the first line of the component definition
its similar in behavior but it isn't the same
Blue orchard beeOP
Oh damn I see
That is subtle!
yeah, it tries to "unify" the semantics into one but i believe thats the only subtlety you need to know.
- use cache in top-level
- use cache in components
- use cache in functions
in which case the component and function needs to be async
- use cache in top-level
- use cache in components
- use cache in functions
in which case the component and function needs to be async
but think of it as cache boundaries instead of a "trickle down, cache all stuff inside" unlike "use server" or "use client"
if you do write it like this:
think of it like, who owns the cache boundary? since this component doesn't have "use cache" therefore it looks up the tree and see it uses Home's cache boundary.
Therefore if Home's re-renderd, then AltTime and TimeComponent will also regenerate even though AltTimeComponent's tag wasn't invoked.
Therefore if Home's re-renderd, then AltTime and TimeComponent will also regenerate even though AltTimeComponent's tag wasn't invoked.
the same goes with "use client" and "use server". it looks up and see who owns the boundaries. (but there is much more stricter nuance with "use server" but the idea is the same)
Blue orchard beeOP
Gotcha!
Okay so actually the recreate the behaviour of the unstable_cache is quite straight forward if I add 'use cache' to the top of the page and then in my fetch functions. calling updateTag on the syncTags directly will invalidate the cache of that fetch function and it should indeed not be affected by the top level use cache
yeah "use cache" at the top of the page is something thats exclusive on its own and more related to
people have reported that they now have to add "use cache" to top of the file everywhere to keep their app to work in cacheComponent when migrating, regardless if they used unstable_cache or not
cacheComponent: true rather than 1-to-1 conversion from unstable_cache.people have reported that they now have to add "use cache" to top of the file everywhere to keep their app to work in cacheComponent when migrating, regardless if they used unstable_cache or not
Blue orchard beeOP
Yeah I can imagine
But okay, thanks so much for taking the time to go through this with me!
I will get to refactoring everything now haha!
no problem! thank you for being patient with me!
Blue orchard beeOP
Vice versa!