-
Notifications
You must be signed in to change notification settings - Fork 26.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Wire AsyncLocalStorage within a cached context (#70573)
This ensures we exit the RequestStorage/PrerenderStorage context and anything else but keeps the StaticGenerationStore (which should no longer have page/request specific information). I also create new CacheStore which will be used for life/tags but for now is just used to provide a better error when cookies/headers/draftMode are accessed within the cache scope. I tried to make `React.cache` AsyncLocalStorage scope be shared between the invocation of the cached function and anything it renders later. This is also necessary in case the cached function calls Float methods like `preload()`. However, I hit issues with preserving debug info, having the right owner and that rendering a Promise in a Server Component turns it into a `React.lazy` for now which doesn't preserve the type we want. The proper solution is for React to expose a larger scope for `React.cache` and float.
- Loading branch information
1 parent
e5a0368
commit b8d1ef7
Showing
14 changed files
with
309 additions
and
10 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
--- | ||
title: Cannot access `cookies()` or `headers()` in `"use cache"` | ||
--- | ||
|
||
#### Why This Error Occurred | ||
|
||
A function is trying to read from the current incoming request inside the scope of a function annotated with `"use cache"`. This is not supported because it would make the cache invalidated by every request which is probably not what you intended. | ||
|
||
#### Possible Ways to Fix It | ||
|
||
Instead of calling this inside the `"use cache"` function, move it outside the function and pass the value in as an argument. The specific value will now be part of the cache key through its arguments. | ||
|
||
Before: | ||
|
||
```jsx filename="app/page.js" | ||
import { cookies } from 'next/headers' | ||
|
||
async function getExampleData() { | ||
"use cache" | ||
- const isLoggedIn = (await cookies()).has('token') | ||
... | ||
} | ||
|
||
export default async function Page() { | ||
const data = await getExampleData() | ||
return ... | ||
} | ||
``` | ||
|
||
After: | ||
|
||
```jsx filename="app/page.js" | ||
import { cookies } from 'next/headers' | ||
|
||
async function getExampleData(isLoggedIn) { | ||
"use cache" | ||
... | ||
} | ||
|
||
export default async function Page() { | ||
+ const isLoggedIn = (await cookies()).has('token') | ||
const data = await getExampleData(isLoggedIn) | ||
return ... | ||
} | ||
``` | ||
|
||
### Useful Links | ||
|
||
- [`headers()` function](https://nextjs.org/docs/app/api-reference/functions/headers) | ||
- [`cookies()` function](https://nextjs.org/docs/app/api-reference/functions/cookies) | ||
- [`draftMode()` function](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
This file contains 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
This file contains 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
4 changes: 4 additions & 0 deletions
4
packages/next/src/server/app-render/cache-async-storage-instance.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import type { CacheAsyncStorage } from './cache-async-storage.external' | ||
import { createAsyncLocalStorage } from '../../client/components/async-local-storage' | ||
|
||
export const cacheAsyncStorage: CacheAsyncStorage = createAsyncLocalStorage() |
15 changes: 15 additions & 0 deletions
15
packages/next/src/server/app-render/cache-async-storage.external.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { AsyncLocalStorage } from 'async_hooks' | ||
|
||
// Share the instance module in the next-shared layer | ||
import { cacheAsyncStorage } from './cache-async-storage-instance' with { 'turbopack-transition': 'next-shared' } | ||
|
||
/** | ||
* The Cache store is for tracking information inside a "use cache" or unstable_cache context. | ||
* Inside this context we should never expose any request or page specific information. | ||
*/ | ||
export type CacheStore = { | ||
// TODO: Inside this scope we'll track tags and life times of this scope. | ||
} | ||
|
||
export type CacheAsyncStorage = AsyncLocalStorage<CacheStore> | ||
export { cacheAsyncStorage } |
6 changes: 6 additions & 0 deletions
6
packages/next/src/server/app-render/clean-async-snapshot-instance.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { createSnapshot } from '../../client/components/async-local-storage' | ||
|
||
export const runInCleanSnapshot: <R, TArgs extends any[]>( | ||
fn: (...args: TArgs) => R, | ||
...args: TArgs | ||
) => R = createSnapshot() |
4 changes: 4 additions & 0 deletions
4
packages/next/src/server/app-render/clean-async-snapshot.external.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Share the instance module in the next-shared layer | ||
import { runInCleanSnapshot } from './clean-async-snapshot-instance' with { 'turbopack-transition': 'next-shared' } | ||
|
||
export { runInCleanSnapshot } |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use client' | ||
|
||
import { Component } from 'react' | ||
|
||
export default class ErrorBoundary extends Component< | ||
{ id: string; children: React.ReactNode }, | ||
{ message: null | string } | ||
> { | ||
state = { message: null } | ||
static getDerivedStateFromError(error: any) { | ||
return { message: error.message } | ||
} | ||
render() { | ||
if (this.state.message !== null) { | ||
return <p id={this.props.id}>{this.state.message}</p> | ||
} | ||
return this.props.children | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import ErrorBoundary from './error-boundary' | ||
|
||
import { cookies } from 'next/headers' | ||
|
||
import { currentUser, currentReferer, isEditing } from './util' | ||
|
||
async function Cookie() { | ||
'use cache' | ||
return <div>User: {currentUser()}</div> | ||
} | ||
|
||
async function Header() { | ||
'use cache' | ||
return <div>Referer: {currentReferer()}</div> | ||
} | ||
|
||
async function DraftMode() { | ||
'use cache' | ||
return <div>Editing: {isEditing()}</div> | ||
} | ||
|
||
export default async function Page() { | ||
await cookies() | ||
return ( | ||
<div> | ||
<ErrorBoundary id="cookies"> | ||
<Cookie /> | ||
</ErrorBoundary> | ||
<ErrorBoundary id="headers"> | ||
<Header /> | ||
</ErrorBoundary> | ||
<ErrorBoundary id="draft-mode"> | ||
<DraftMode /> | ||
</ErrorBoundary> | ||
</div> | ||
) | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { cookies, headers, draftMode } from 'next/headers' | ||
|
||
export async function currentUser() { | ||
return (await cookies()).get('user')?.value | ||
} | ||
|
||
export async function currentReferer() { | ||
return (await headers()).get('referer') | ||
} | ||
|
||
export async function isEditing() { | ||
return (await draftMode()).isEnabled | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,11 @@ | ||
import { Suspense } from 'react' | ||
|
||
export default function Root({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html> | ||
<body>{children}</body> | ||
<body> | ||
<Suspense fallback={null}>{children}</Suspense> | ||
</body> | ||
</html> | ||
) | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { cache } from 'react' | ||
|
||
const number = cache(() => { | ||
return Math.random() | ||
}) | ||
|
||
function Component() { | ||
// Read number again in a component. This should be deduped. | ||
return <p id="b">{number()}</p> | ||
} | ||
|
||
async function getCachedComponent() { | ||
'use cache' | ||
return ( | ||
<div> | ||
<p id="a">{number()}</p> | ||
<Component /> | ||
</div> | ||
) | ||
} | ||
|
||
export default async function Page() { | ||
return <div>{getCachedComponent()}</div> | ||
} |
Oops, something went wrong.