next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Docs: [Cache Components] "use cache" pitfalls with map()

Open gaearon opened this issue 1 month ago • 6 comments

What is the documentation issue?

Here's a fix I'm going to deploy now:

 const resolveDidToHandle = cache(async function resolveDidToHandle(did: string): Promise<string> {
   "use cache: redis";
   tagDid(did);
@@ -156,7 +160,7 @@ export const resolveHandleToDid = cache(async function resolveHandleToDid(
 
 async function batchResolveHandles(dids: string[]): Promise<Map<string, string>> {
   const unique = [...new Set(dids)];
-  const results = await Promise.all(unique.map(resolveDidToHandle));
+  const results = await Promise.all(unique.map((did) => resolveDidToHandle(did)));
   return new Map(unique.map((did, i) => [did, results[i]]));
 }
 
@@ -191,8 +195,8 @@ async function batchResolveUsers(dids: string[]): Promise<Map<string, User>> {
   if (unique.length === 0) return new Map();
 
   const [handles, avatars] = await Promise.all([
-    Promise.all(unique.map(resolveDidToHandle)),
-    Promise.all(unique.map(tryFetchBskyAvatar)),
+    Promise.all(unique.map((did) => resolveDidToHandle(did))),
+    Promise.all(unique.map((did) => tryFetchBskyAvatar(did))),
   ]);

Without this fix, my Redis cache handler wasn't working properly.

Can you spot why?

Is there any context that might help us understand?

I'm honestly not sure if this is something to call out in the docs, if this could be linted against, or something else. But it was extremely confusing. (To clarify, the issue is that map sends the array and the index to the mapped function, which implicitly become cache keys, blowing up the cache size and making it miss often.)

For what it's worth, a mode that simply logs the captured cache keys would've helped here.

Does the docs page already exist? Please link to it.

https://nextjs.org/docs/app/api-reference/directives/use-cache#cache-keys

gaearon avatar Dec 06 '25 08:12 gaearon

I guess this isn’t specific to “use cache” and would’ve been an issue in any approach with var arg memoization. And I could’ve logged it in my cache handler if I looked closer at the keys and figured out what’s going on.

I’m not sure if there’s anything actionable here but I do think a note in Troubleshooting could help.

gaearon avatar Dec 06 '25 09:12 gaearon

Very good point. I guess something like this could help (with the map example included):

Since "use cache" will use the arguments given to the cached function as cache keys, it is a good practice to always send those arguments explicitly. Passing cached functions into callbacks could lead to the cache function retrieving additional unneeded arguments and thus overly specific cache keys, leading to a bad cache behaviour. Consider map for example:

...

pmk1c avatar Dec 06 '25 11:12 pmk1c

For what it's worth, a mode that simply logs the captured cache keys would've helped here.

You can use NEXT_PRIVATE_DEBUG_CACHE=1 to log the keys. This is currently only documented in this context, I think. @icyJoseph We might wanna mention that in a 'use cache' context as well.

unstubbable avatar Dec 06 '25 12:12 unstubbable

Hmm, actually, we should be able to ignore those arguments based on our static analysis that the parameters are not defined (and arguments being forbidden). 🤔 I'll see if I can come up with a fix.

unstubbable avatar Dec 06 '25 12:12 unstubbable

As a side note: we're already ignoring unused arguments when cache functions are called from the client (via #72506), but not yet when called from the server. I have an idea on how to fix this at the compiler level.

unstubbable avatar Dec 06 '25 13:12 unstubbable

I have an idea on how to fix this at the compiler level.

Implemented in #86920.

unstubbable avatar Dec 07 '25 23:12 unstubbable