docs: improve clarity in cache components and server/client docs (#88946)

- Applying learnings from a recipe
- React.cache usage under a use cache scope
This commit is contained in:
Joseph
2026-01-26 09:51:39 +01:00
committed by GitHub
parent a57e4e809b
commit 0a6b9ba5b3
3 changed files with 221 additions and 0 deletions
@@ -412,6 +412,191 @@ Your Server Component will now be able to directly render your provider, and all
> **Good to know**: You should render providers as deep as possible in the tree – notice how `ThemeProvider` only wraps `{children}` instead of the entire `<html>` document. This makes it easier for Next.js to optimize the static parts of your Server Components.
### Sharing data with context and React.cache
You can share fetched data across both Server and Client Components by combining [`React.cache`](https://react.dev/reference/react/cache) with context providers.
Create a cached function that fetches data:
```ts filename="app/lib/user.ts" switcher
import { cache } from 'react'
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user')
return res.json()
})
```
```js filename="app/lib/user.js" switcher
import { cache } from 'react'
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user')
return res.json()
})
```
Create a context provider that stores the promise:
```tsx filename="app/user-provider.tsx" switcher
'use client'
import { createContext } from 'react'
type User = {
id: string
name: string
}
export const UserContext = createContext<Promise<User> | null>(null)
export default function UserProvider({
children,
userPromise,
}: {
children: React.ReactNode
userPromise: Promise<User>
}) {
return <UserContext value={userPromise}>{children}</UserContext>
}
```
```jsx filename="app/user-provider.js" switcher
'use client'
import { createContext } from 'react'
export const UserContext = createContext(null)
export default function UserProvider({ children, userPromise }) {
return <UserContext value={userPromise}>{children}</UserContext>
}
```
In a layout, pass the promise to the provider without awaiting:
```tsx filename="app/layout.tsx" switcher
import UserProvider from './user-provider'
import { getUser } from './lib/user'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const userPromise = getUser() // Don't await
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
```
```jsx filename="app/layout.js" switcher
import UserProvider from './user-provider'
import { getUser } from './lib/user'
export default function RootLayout({ children }) {
const userPromise = getUser() // Don't await
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
```
Client Components use [`use()`](https://react.dev/reference/react/use) to resolve the promise from context, wrapped in `<Suspense>` for fallback UI:
```tsx filename="app/ui/profile.tsx" switcher
'use client'
import { use, useContext } from 'react'
import { UserContext } from '../user-provider'
export function Profile() {
const userPromise = useContext(UserContext)
if (!userPromise) {
throw new Error('useContext must be used within a UserProvider')
}
const user = use(userPromise)
return <p>Welcome, {user.name}</p>
}
```
```jsx filename="app/ui/profile.js" switcher
'use client'
import { use, useContext } from 'react'
import { UserContext } from '../user-provider'
export function Profile() {
const userPromise = useContext(UserContext)
if (!userPromise) {
throw new Error('useContext must be used within a UserProvider')
}
const user = use(userPromise)
return <p>Welcome, {user.name}</p>
}
```
```tsx filename="app/page.tsx" switcher
import { Suspense } from 'react'
import { Profile } from './ui/profile'
export default function Page() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
)
}
```
```jsx filename="app/page.js" switcher
import { Suspense } from 'react'
import { Profile } from './ui/profile'
export default function Page() {
return (
<Suspense fallback={<div>Loading profile...</div>}>
<Profile />
</Suspense>
)
}
```
Server Components can also call `getUser()` directly:
```tsx filename="app/dashboard/page.tsx" switcher
import { getUser } from '../lib/user'
export default async function DashboardPage() {
const user = await getUser() // Cached - same request, no duplicate fetch
return <h1>Dashboard for {user.name}</h1>
}
```
```jsx filename="app/dashboard/page.js" switcher
import { getUser } from '../lib/user'
export default async function DashboardPage() {
const user = await getUser() // Cached - same request, no duplicate fetch
return <h1>Dashboard for {user.name}</h1>
}
```
Since `getUser` is wrapped with `React.cache`, multiple calls within the same request return the same memoized result, whether called directly in Server Components or resolved via context in Client Components.
> **Good to know**: `React.cache` is scoped to the current request only. Each request gets its own memoization scope with no sharing between requests.
### Third-party components
When using a third-party component that relies on client-only features, you can wrap it in a Client Component to ensure it works as expected.
@@ -194,6 +194,10 @@ Use [`connection()`](/docs/app/api-reference/functions/connection) if you need t
> **Good to know**: Runtime data cannot be cached with `use cache` because it requires request context. Components that access runtime APIs must always be wrapped in `<Suspense>`. However, you can extract values from runtime data and pass them as arguments to cached functions. See the [with runtime data](#with-runtime-data) section for an example.
One approach for reading runtime data like cookies without blocking the static shell is to pass a promise to a client context provider. See [Sharing data with context and React.cache](/docs/app/getting-started/server-and-client-components#sharing-data-with-context-and-reactcache) for an example.
> **Good to know:** `React.cache` operates in an isolated scope inside `use cache` boundaries. See [React.cache isolation](/docs/app/api-reference/directives/use-cache#reactcache-isolation) for more information.
### Non-deterministic operations
Operations like `Math.random()`, `Date.now()`, or `crypto.randomUUID()` produce different values each time they execute. To ensure these run at request time (generating unique values per request), Cache Components requires you to explicitly signal this intent by calling these operations after dynamic or runtime data access.
@@ -189,6 +189,10 @@ async function CachedForm({ action }: { action: () => Promise<void> }) {
## Constraints
Cached functions execute in an isolated environment. The following constraints ensure cache behavior remains predictable and secure.
### Runtime APIs
Cached functions and components **cannot** directly access runtime APIs like `cookies()`, `headers()`, or `searchParams`. Instead, read these values outside the cached scope and pass them as arguments.
### Runtime caching considerations
@@ -206,6 +210,34 @@ If the default in-memory cache isn't enough, consider **[`use cache: remote`](/d
Very rarely, for compliance requirements or when you can't refactor your code to pass runtime data as arguments to a `use cache` scope, you might need [`use cache: private`](/docs/app/api-reference/directives/use-cache-private).
### React.cache isolation
[`React.cache`](https://react.dev/reference/react/cache) operates in an isolated scope inside `use cache` boundaries. Values stored via `React.cache` outside a `use cache` function are not visible inside it.
This means you cannot use `React.cache` to pass data into a `use cache` scope:
```tsx
import { cache } from 'react'
const store = cache(() => ({ current: null as string | null }))
function Parent() {
const shared = store()
shared.current = 'value from parent'
return <Child />
}
async function Child() {
'use cache'
const shared = store()
// shared.current is null, not 'value from parent'
// use cache has its own isolated React.cache scope
return <div>{shared.current}</div>
}
```
This isolation ensures cached functions have predictable, self-contained behavior. To pass data into a `use cache` scope, use function arguments instead.
## `use cache` at runtime
On the **server**, cache entries are stored in-memory and respect the `revalidate` and `expire` times from your `cacheLife` configuration. You can customize the cache storage by configuring [`cacheHandlers`](/docs/app/api-reference/config/next-config-js/cacheHandlers) in your `next.config.js` file.