docs: scope React cache() guidance to Server Components in the auth guide and after() reference - #98579
Open
henryjayyu wants to merge 4 commits into
Open
docs: scope React cache() guidance to Server Components in the auth guide and after() reference#98579henryjayyu wants to merge 4 commits into
henryjayyu wants to merge 4 commits into
Conversation
…erver Actions
The DAL example is cache()-wrapped and the guide then points readers at Server
Actions and Route Handlers, where cache() is a no-op: it evaluates the function
and neither reads nor writes the cache, with no error and no warning.
Also adds the missing cache/redirect imports to the DAL snippet, and guards the
role access in the verifySession-based examples. verifySession() returns
{ isAuth, userId }, so session.user.role throws; the Server Component examples
already guard it the same way.
…ponents The bullet was unqualified, on a page that lists Server Components, Server Functions, Route Handlers and Proxy as supported contexts. Deduplication only happens in the first. Also records what does happen in a Server Component: the after callback shares the render pass's cache, so a value already computed during the render is not recomputed.
The 401 branch was unreachable. verifySession() either calls redirect(), which throws, or returns a truthy object — it never returns a falsy value — so the only failure a Route Handler could actually produce was a redirect to /login, from an endpoint the same section says to treat like a public-facing API. Adds a non-redirecting getSession() to the DAL and uses it in the Route Handler example, so the documented 401 is reachable and a programmatic caller gets a status code instead of a navigation. verifySession() is unchanged and still the right choice for pages. getSession() is deliberately not cache()-wrapped: its callers do not render, so the wrapper would do nothing.
Every App Router example read session.user.role, but nothing in the guide ever
produces a nested user object: verifySession() returns { isAuth, userId }, so
the access was undefined and the role checks could never pass. The sibling Cache
Components guide models the session flat as well (SessionData = { userId }).
Reads session.role instead, and has verifySession()/getSession() forward the
role from the decrypted payload — decrypt() returns the raw JWT payload, so it
was already reachable and simply being dropped. This is the shape the guide's
own Tips section asks for: 'The payload should contain the minimum, unique user
data ... such as the user's ID, role, etc.'
createSession() is left alone: whether a role belongs in the payload is the
reader's decision, so the examples keep an explicit 'assuming you included role
in the session payload' comment rather than the guide presuming a role column.
The Pages Router examples use a different getSession(req) from an auth library
and are untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two docs pages recommend React
cache()in contexts where it does not memoize. This qualifies both, and fixes three smaller defects in the examples I had to touch anyway.Previously filed as #98577 / #98578, both auto-closed by the triage bot for a missing reproduction link — they were created through the API, which bypasses the issue template and so never set the
Documentationtype the bot checks for. Opening a PR instead, as the docs report template suggests.Why
React's reference says
cache"is for use in Server Components only," and that calling a memoized function outside a component "will still evaluate the function but not read or update the cache." There is no error and no warning — the wrapper simply does nothing.Measured on Next.js 16.3.4 / React 19.2.4, counting executions of the underlying function and comparing returned object identity:
afteraftershares the body's cacheConsistent with
packages/next/src/server/route-modules/app-route/module.tscontaining no reference to React at all — noReactSharedInternals, nogetCacheForType— socache()falls through to the pass-through in React'sReactCacheImpl.js.This is not hypothetical. Following the Authentication guide's pattern in a production app, an auth wrapper resolved the caller once and then every service-layer call inside the handler re-resolved it, because the
cache()wrapper did nothing: oneGETperformed three session decrypts and three identical permission-user database queries where one of each was needed. Diagnosing it required reading the compiled runtime, precisely because the documented pattern looks correct.Changes
guides/authentication.mdxverifySession()function in your data requests, Server Actions, and Route Handlers" —cacheonly memoizes during a render pass, so it gives Server Actions and Route Handlers nothing. The page already uses the right phrase elsewhere ("during a React render pass"); it just was not carried into the sentence naming the call sites.cacheandredirectimports to both DAL snippets.session.user.rolein the threeverifySession-based examples.verifySession()returns{ isAuth, userId }— there is nouserproperty, so the unguarded access throws a TypeError. The Server Component examples already guard it withsession?.user?.role, so this just makes the Server Action and Route Handler examples consistent. The Pages Router examples use a differentgetSession(req)and are untouched.api-reference/functions/after.mdxcachebullet to Server Components. The page lists Server Components, Server Functions, Route Handlers and Proxy as supported contexts, and the bullet sat a few lines below that list, unqualified. Also records what does happen in a Server Component, which is better than the original promised: the callback shares the render pass's cache, so a value already computed during the render is not recomputed.Prettier clean under the repo's
.prettierrc.json.The Route Handler example's unreachable 401
The example previously read:
verifySession()either callsredirect(), which throws, or returns a truthy object — it never returns a falsy value, so the 401 was dead code. The only failure a Route Handler could actually produce was a redirect to/login, from an endpoint the same section says to "treat with the same security considerations as public-facing API endpoints."Fixed by adding a non-redirecting
getSession()to the DAL and using it in the Route Handler example, so the documented 401 is reachable and a programmatic caller gets a status code rather than a navigation.verifySession()is unchanged and remains the right choice for pages, which is where a redirect belongs.getSession()is deliberately notcache()-wrapped — its callers do not render, so the wrapper would do nothing, which is the point of the rest of this PR.I took this direction rather than the alternative (leave the redirect and delete the 401) because the guide's own framing of Route Handlers as public-facing API endpoints argues for it. Happy to flip it if you disagree — it is a small commit either way.
The role shape
Every App Router example read
session.user.role, but nothing in the guide ever produces a nesteduserobject —verifySession()returns{ isAuth, userId }. So the access wasundefined, the role checks could never pass, and before this PR it threw a TypeError outright.Checked against the sibling Authentication with Cache Components guide, which models the session flat too:
So the fix is to read
session.role, and to haveverifySession()/getSession()forward the role from the decrypted payload.decrypt()returns the raw JWT payload, so the value was already reachable — the DAL was simply dropping it. That matches what the guide's own Tips section asks for:createSession()is deliberately left alone. Whether a role belongs in the payload is the reader's decision, and threading one through would mean presuming arolecolumn in the signup example's insert. The examples keep an explicit// Assuming you included 'role' in the session payloadcomment instead — the same caveat the guide already had, now attached to a path that works.The two Pages Router examples use a different
getSession(req)from an auth library and are untouched.