Ask
34

Next.js 16.3 preview: "use cache" breaks i18next with Cannot access cookies() or headers() in "use cache"

Trying the instant navigation work on the preview channel, so npm install next@preview as of this week, cacheComponents: true in next.config.ts, and 'use cache' added to layouts and pages so the static shell gets prerendered.

Everything that touches i18next falls over, in two different shapes:

Shape 1, when the i18n setup resolves the locale from the request:

Cannot access cookies() or headers() in "use cache"

Shape 2, when I try to be clever and hand it a promise instead of an awaited value: the build sits there for about a minute and then dies with

Error: Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or uncached data were used inside "use cache".

The annoying part is that my translations do not depend on the request at all. They are JSON bundles that could be resolved at build time and never change between users. The only per-request thing in the whole chain is which locale, and even that is in the URL.

So: what is the shape that actually works with cacheComponents on, and is i18next simply the wrong library for this right now? Wrapping the init in my own helper makes the error go away but I suspect I have just moved the dynamic read somewhere I cannot see it.

7 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @etcd_eli · 2h ago · 2 replies

    The rule is simple once you accept it: a cached scope cannot read cookies(), headers() or searchParams. You read them outside and pass the value in as an argument, and that argument becomes part of the cache key. The docs are blunt about this, and the error page for it says exactly that.

    For i18n that means the locale must arrive as a plain value, not be discovered inside the cached function:

    1. Put the locale in the route segment, app/[locale]/..., so it comes from params rather than from a header.
    2. Read it in the uncached page/layout, then call your cached loader with it: const messages = await getMessages(locale) where getMessages is the thing marked 'use cache'.
    3. Keep negotiation (Accept-Language sniffing, cookie preference, redirects) in middleware or in an uncached boundary. Negotiation is per-request by definition and has no business inside a cache entry.

    Done that way you get one cache entry per locale, which is exactly what you want, and the shell prerenders.

    26
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @goroutine_gil · 2h ago

      Worth adding the serialization trap, since it is the next wall after this one. Arguments to a cached function have to be serializable, and class instances are explicitly not. An initialised i18next instance is a class instance, so passing one in will not work even though it looks like "just data".

      Pass the locale string in and construct or look up the instance inside the cached function, and return plain objects or JSX rather than the instance itself.

      9
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @rollback_rae · 2h ago

    There is an escape hatch and you should know why not to use it here. 'use cache: private' is documented as the option for when you cannot refactor the runtime read out, and it does allow cookies(), headers() and searchParams inside the scope.

    The catch: results are never stored on the server, only in browser memory, they do not survive a reload, and the function is excluded from static shell generation. It is also still marked experimental.

    So for the thing you are actually chasing, which is a prerendered instant shell, private gives you the opposite. Use it for genuinely per-user fragments, not for translations that are the same for everyone in a locale.

    17
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @regexafterdark · 2h ago

    Ignore anyone telling you your t wrapper is calling itself. If you had genuinely written infinite recursion you would get a stack overflow, not a cache error. Read the stack in the terminal down to the first frame in your code before you rewrite the wrapper.

    The structural fix that made this painless for me: import the message JSON statically so it is bundled at build time, and let only the locale flow through the app as a string. Once no part of the translation path is doing IO or reading the request, there is nothing left for the cache to complain about.

    8
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @static_typing_fan · 7h ago · 2 replies

    Easiest fix: create the i18n instance once, wrap it in React.cache() outside the component tree, and read it from inside the 'use cache' function. Same request, same instance, no dynamic API touched.

    11
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @hot_heap_hana · 2h ago

      This does not work, and the failure is confusing rather than loud, so it is worth being precise. React.cache operates in an isolated scope inside use cache boundaries. Values stored via React.cache outside a cached function are not visible inside it, and the docs give a worked example where the value reads back as null.

      The only channel into a cached scope is arguments, and captured closure variables count, because they get bound as arguments and become part of the key. If you want something in there, it has to be passed in as a serializable value.

      14
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @wrangler_dev_jo · 2h ago

    On the library question, honestly: i18next was not designed around this constraint and you feel it. Two data points rather than a recommendation.

    next-intl has been publishing guidance for the Cache Components world, and there is an open issue where its Link still triggers a request-context lookup inside a cached scope even when you pass the locale explicitly, so it is not a finished story either. Compile-time approaches like paraglide sidestep the whole problem because messages become plain imports with nothing dynamic to trip over.

    I have only run the compile-time route on a small app. Before migrating anything large, go and read the current open issues for whichever one you pick, because this is all moving week to week.

    6
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report