#cache comps

1 messages · Page 1 of 1 (latest)

silver totem
#

I'm trying to upgrade a codebase to use cache components. A common pattern in the code is to pass around a ctx argument with db client (regular/transaction), logger etc. These can not be serialised and thus can't be part of the arguments passed to a cache component. I tried to solve this by splitting it into a separate function but still run into this error

let count = 0;

class SomeClass {}

const wrapper = <T extends (...args: any[]) => Promise<any>>(
fn: (ctx: { someClass: SomeClass }) => T,
): T => fn({ someClass: new SomeClass() });

const getCount = wrapper((ctx) =>
// ^
// ⨯ Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.
async () => {
"use cache";
console.log(ctx.someClass);
count++;
return count;
},
);

export default async function CacheCompTestPage() {
const localCount = await getCount();
return (
<div>
<p>Count: {localCount}</p>
</div>
);
}

Is the only way around this to use async local storage?

pastel whaleBOT
#

Hi @silver totem. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You’re running into a documented limitation of use cache: both its arguments and any closed‑over values must be serializable according to the React Server Components rules. Class instances (like new SomeClass()) are explicitly not supported as arguments or captured values. [use cache serialization]

In your example:

const getCount = wrapper((ctx) =>
  async () => {
    "use cache";
    console.log(ctx.someClass);
    count++;
    return count;
  },
);

ctx.someClass is captured from the outer scope of the cached function, so it becomes part of the cache key input and must be serializable. That’s why you get the “Only plain objects, and a few built‑ins…” error. [use cache how-it-works]

The docs describe two relevant patterns:

  1. Avoid passing dynamic / non‑serializable inputs into cached scopes
    You must not pass runtime data (cookies, headers, etc.) or other non‑serializable values into use cache scopes unless you only pass them through and never read them. [cache components with runtime data; avoid dynamic inputs]

  2. Pass‑through only (no introspection)
    Non‑serializable values (like children or Server Actions) can be accepted only if you don’t read or modify them inside the cached function – they’re just forwarded. [use cache serialization; interleaving example]

#

In your code you do read ctx.someClass (console.log(ctx.someClass)), so it cannot be treated as a pass‑through value.

From the docs, the supported pattern for runtime / non‑serializable context is:

  • Keep the cached function/component free of non‑serializable context.
  • Read runtime / contextual data outside the cached scope and pass only serializable pieces (IDs, config objects, etc.) into the cached function. [cache components with runtime data]

Example from the docs adapted to your case:

// Non-cached function that has access to full ctx (db, logger, etc.)
async function getCountWithCtx(ctx: { someClass: SomeClass }) {
  // Derive only serializable data to pass into the cached function
  const someId = ctx.someClass.id; // must be serializable

  return getCountCached(someId);
}

// Cached function only sees serializable inputs
async function getCountCached(someId: string) {
  "use cache";
  // use someId, but not the class instance
  count++;
  return count;
}

Regarding AsyncLocalStorage: the provided docs only mention it as a Next.js polyfill for the Edge runtime, not as a recommended pattern for carrying non‑serializable context into use cache scopes. [Next.js polyfills] There is no explicit guidance in the knowledge sources that AsyncLocalStorage is the “intended” or “only” way to solve this, so I can’t state that confidently.

What is clearly recommended:

#
  • Structure your cached functions so that:
    • They only receive serializable arguments.
    • Any non‑serializable context (db client, logger, class instances) is used outside the use cache scope to derive serializable inputs for the cached function. [use cache constraints; with runtime data]

If you need to both use the full ctx and cache some derived result, the pattern from the docs is essentially:

  1. Non‑cached wrapper that has ctx and calls…
  2. A cached function that only takes serializable arguments and does not close over ctx or class instances.

Anything beyond that (like using AsyncLocalStorage to hide non‑serializable context) is not covered in the provided sources, so I can’t recommend it based on this documentation.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: