diff --git a/docs/01-app/03-api-reference/01-directives/use-cache-remote.mdx b/docs/01-app/03-api-reference/01-directives/use-cache-remote.mdx new file mode 100644 index 0000000000000..eb758ad2fb2dd --- /dev/null +++ b/docs/01-app/03-api-reference/01-directives/use-cache-remote.mdx @@ -0,0 +1,560 @@ +--- +title: 'use cache: remote' +description: 'Learn how to use the `"use cache: remote"` directive to enable caching in dynamic contexts in your Next.js application.' +version: canary +related: + title: Related + description: View related API references. + links: + - app/api-reference/directives/use-cache + - app/api-reference/directives/use-cache-private + - app/api-reference/config/next-config-js/cacheComponents + - app/api-reference/functions/cacheLife + - app/api-reference/functions/cacheTag + - app/api-reference/functions/connection +--- + +The `'use cache: remote'` directive enables caching of **shared data** in dynamic contexts where regular [`use cache`](/docs/app/api-reference/directives/use-cache) would not work, for example after calling [`await connection()`](/docs/app/api-reference/functions/connection), [`await cookies()`](/docs/app/api-reference/functions/cookies) or [`await headers()`](/docs/app/api-reference/functions/headers). + +> **Good to know:** +> +> - Results are stored in server-side cache handlers and shared across all users. +> - For **user-specific data** that depends on [`await cookies()`](/docs/app/api-reference/functions/cookies) or [`await headers()`](/docs/app/api-reference/functions/headers), use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) instead. + +## Usage + +To use `'use cache: remote'`, enable the [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) flag in your `next.config.ts` file: + +```ts filename="next.config.ts" switcher +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, +} + +export default nextConfig +``` + +```js filename="next.config.js" switcher +/** @type {import('next').NextConfig} */ +const nextConfig = { + cacheComponents: true, +} + +export default nextConfig +``` + +Then add `'use cache: remote'` to your function that needs to cache data in a dynamic context. + +### Basic example + +Cache product pricing that needs to be fetched at request time but can be shared across all users. Use [`cacheLife`](/docs/app/api-reference/functions/cacheLife#custom-cache-profiles) to set the cache lifetime of the price. + +```tsx filename="app/product/[id]/page.tsx" switcher +import { Suspense } from 'react' +import { connection } from 'next/server' +import { cacheTag, cacheLife } from 'next/cache' + +export default async function ProductPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + + return ( +
+ + Loading price...
}> + + + + ) +} + +function ProductDetails({ id }: { id: string }) { + return
Product: {id}
+} + +async function ProductPrice({ productId }: { productId: string }) { + // Calling connection() makes this component dynamic, preventing + // it from being included in the static shell. This ensures the price + // is always fetched at request time. + await connection() + + // Now we can cache the price in a remote cache handler. + // Regular 'use cache' would NOT work here because we're in a dynamic context. + const price = await getProductPrice(productId) + + return
Price: ${price}
+} + +async function getProductPrice(productId: string) { + 'use cache: remote' + cacheTag(`product-price-${productId}`) + cacheLife({ expire: 3600 }) // 1 hour + + // This database query is cached and shared across all users + return db.products.getPrice(productId) +} +``` + +```jsx filename="app/product/[id]/page.js" switcher +import { Suspense } from 'react' +import { connection } from 'next/server' +import { cacheTag, cacheLife } from 'next/cache' + +export default async function ProductPage({ params }) { + const { id } = await params + + return ( +
+ + Loading price...
}> + + + + ) +} + +function ProductDetails({ id }) { + return
Product: {id}
+} + +async function ProductPrice({ productId }) { + // Calling connection() makes this component dynamic, preventing + // it from being included in the static shell. This ensures the price + // is always fetched at request time. + await connection() + + // Now we can cache the price in a remote cache handler. + // Regular 'use cache' would NOT work here because we're in a dynamic context. + const price = await getProductPrice(productId) + + return
Price: ${price}
+} + +async function getProductPrice(productId) { + 'use cache: remote' + cacheTag(`product-price-${productId}`) + cacheLife({ expire: 3600 }) // 1 hour + + // This database query is cached and shared across all users + return db.products.getPrice(productId) +} +``` + +> **Note:** Regular [`use cache`](/docs/app/api-reference/directives/use-cache) will not cache anything when used in a dynamic context (after [`await connection()`](/docs/app/api-reference/functions/connection), [`await cookies()`](/docs/app/api-reference/functions/cookies), [`await headers()`](/docs/app/api-reference/functions/headers), etc.). Use `'use cache: remote'` to enable runtime caching in these scenarios. + +## How `use cache: remote` differs from `use cache` and `use cache: private` + +Next.js provides three caching directives, each designed for different use cases: + +| Feature | `use cache` | `'use cache: remote'` | `'use cache: private'` | +| -------------------------------- | ----------------------------------- | --------------------------------------------------------- | ----------------------------------- | +| **Works in dynamic context** | No (requires static context) | Yes (designed for dynamic contexts) | Yes | +| **Access to `await cookies()`** | No | No | Yes | +| **Access to `await headers()`** | No | No | Yes | +| **After `await connection()`** | No (won't cache) | No | No | +| **Stored in cache handler** | Yes (server-side) | Yes (server-side) | No (client-side only) | +| **Cache scope** | Global (shared) | Global (shared) | Per-user (isolated) | +| **Supports runtime prefetching** | N/A (pre-rendered at build) | No | Yes (when configured) | +| **Use case** | Static, shared content (build-time) | Dynamic, shared content in runtime contexts (per-request) | Personalized, user-specific content | + +> **Note:** While you can't call `await cookies()` or `await headers()` inside `'use cache: remote'`, you can read the values before calling a function that is wrapped by `'use cache: remote'` and the arguments will be included in the cache key. Note that this is not recommended as it will dramatically increase the cache size and reduce the cache hit rate. + +### When to use each directive + +Choose the right caching directive based on your use case: + +**Use [`use cache`](/docs/app/api-reference/directives/use-cache) when:** + +- Content can be prerendered at build time +- Content is shared across all users +- Content doesn't depend on request-specific data + +**Use `'use cache: remote'` when:** + +- You need caching within dynamic context +- Content is shared across users but must be rendered per-request (after `await connection()`) +- You want to cache expensive operations in a server-side cache handler + +**Use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) when:** + +- Content is personalized per-user (depends on cookies, headers) +- You need [runtime prefetching](/docs/app/guides/prefetching) of user-specific content +- Content should never be shared between users + +## How it works + +The `'use cache: remote'` directive enables runtime caching of shared data in dynamic contexts by storing results in server-side cache handlers rather than prerendering at build time. + +### Dynamic context detection + +When Next.js encounters certain APIs like [`connection()`](/docs/app/api-reference/functions/connection), [`cookies()`](/docs/app/api-reference/functions/cookies), or [`headers()`](/docs/app/api-reference/functions/headers), the context becomes "dynamic". In a dynamic context: + +1. **Regular `use cache` stops working** - it won't cache anything +2. **`'use cache: remote'` continues to work** - it is cached by a remote cache handler. +3. **Results are stored server-side** in a key-value store configured for your deployment +4. **Cached data is shared across requests** - reducing database load and origin requests + +> **Good to know:** Without `'use cache: remote'`, functions in dynamic contexts would execute on every request, potentially creating performance bottlenecks. Remote caching eliminates this issue by storing results in server-side cache handlers. + +### Storage behavior + +Remote caches are **persisted using server-side cache handlers**, which may include: + +- **Distributed key-value stores** (in-memory or persistent storage solutions) +- **File system or in-memory storage** (often used in development or for custom deployments) +- **Environment-specific caches** (provided by your hosting infrastructure) +- **Custom or configured cache handlers** (depending on your application's setup) + +This means: + +1. Cached data is shared across all users and requests +2. Cache entries persist beyond a single session +3. Cache invalidation works via [`cacheTag`](/docs/app/api-reference/functions/cacheTag) and [`revalidateTag`](/docs/app/api-reference/functions/revalidateTag) +4. Cache expiration is controlled by [`cacheLife`](/docs/app/api-reference/functions/cacheLife) configuration + +### Dynamic context example + +```tsx +async function UserDashboard() { + // Calling connection() makes the context dynamic + await connection() + + // Without any caching directive, this runs on every request + const stats = await getStats() + + // With 'use cache: remote', this is cached in the remote handler + const analytics = await getAnalytics() + + return ( +
+ + +
+ ) +} + +async function getAnalytics() { + 'use cache: remote' + cacheLife({ expire: 300 }) // 5 minutes + + // This expensive operation is cached and shared across all requests + return fetchAnalyticsData() +} +``` + +## Request APIs and remote caches + +While `'use cache: remote'` technically allows access to request-specific data by calling API's like [`cookies()`](/docs/app/api-reference/functions/cookies) and [`headers()`](/docs/app/api-reference/functions/headers) before calling a function that is wrapped by `'use cache: remote'`, it's generally not recommended to use them together: + +| API | Allowed in `use cache` | Allowed in `'use cache: remote'` | Recommended | +| ------------------------------------------------------------------------------------- | ---------------------- | -------------------------------- | ------------------------------------------------------------------------------------------ | +| [`cookies()`](/docs/app/api-reference/functions/cookies) | No | No | Use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) instead | +| [`headers()`](/docs/app/api-reference/functions/headers) | No | No | Use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) instead | +| [`connection()`](/docs/app/api-reference/functions/connection) | No | No | No - these cannot ever be cached | +| [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional) | No | No | Use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) instead | + +> **Important:** If you need to cache based on cookies, headers, or search params, use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) instead. Remote caches are shared across all users, so caching user-specific data in them can lead to incorrect results being served to different users. + +## Nesting rules + +Remote caches have specific nesting rules: + +- Remote caches **can** be nested inside other remote caches (`'use cache: remote'`) +- Remote caches **can** be nested inside regular caches (`'use cache'`) +- Remote caches **cannot** be nested inside private caches (`'use cache: private'`) +- Private caches **cannot** be nested inside remote caches + +```tsx +// VALID: Remote inside remote +async function outerRemote() { + 'use cache: remote' + const result = await innerRemote() + return result +} + +async function innerRemote() { + 'use cache: remote' + return getData() +} + +// VALID: Remote inside regular cache +async function outerCache() { + 'use cache' + // If this is in a dynamic context, the inner remote cache will work + const result = await innerRemote() + return result +} + +async function innerRemote() { + 'use cache: remote' + return getData() +} + +// INVALID: Remote inside private +async function outerPrivate() { + 'use cache: private' + const result = await innerRemote() // Error! + return result +} + +async function innerRemote() { + 'use cache: remote' + return getData() +} + +// INVALID: Private inside remote +async function outerRemote() { + 'use cache: remote' + const result = await innerPrivate() // Error! + return result +} + +async function innerPrivate() { + 'use cache: private' + return getData() +} +``` + +## Examples + +The following examples demonstrate common patterns for using `'use cache: remote'`. For details about `cacheLife` parameters (`stale`, `revalidate`, `expire`), see the [`cacheLife` API reference](/docs/app/api-reference/functions/cacheLife). + +### Per-request database queries + +Cache expensive database queries that are accessed in dynamic contexts, reducing load on your database: + +```tsx filename="app/dashboard/page.tsx" +import { connection } from 'next/server' +import { cacheLife, cacheTag } from 'next/cache' + +export default async function DashboardPage() { + // Make context dynamic + await connection() + + const stats = await getGlobalStats() + + return +} + +async function getGlobalStats() { + 'use cache: remote' + cacheTag('global-stats') + cacheLife({ expire: 60 }) // 1 minute + + // This expensive database query is cached and shared across all users, + // reducing load on your database + const stats = await db.analytics.aggregate({ + total_users: 'count', + active_sessions: 'count', + revenue: 'sum', + }) + + return stats +} +``` + +### API responses in streaming contexts + +Cache API responses that are fetched during streaming or after dynamic operations: + +```tsx filename="app/feed/page.tsx" +import { Suspense } from 'react' +import { connection } from 'next/server' +import { cacheLife, cacheTag } from 'next/cache' + +export default async function FeedPage() { + return ( +
+ }> + + +
+ ) +} + +async function FeedItems() { + // Dynamic context + await connection() + + const items = await getFeedItems() + + return items.map((item) => ) +} + +async function getFeedItems() { + 'use cache: remote' + cacheTag('feed-items') + cacheLife({ expire: 120 }) // 2 minutes + + // This API call is cached, reducing requests to your external service + const response = await fetch('https://api.example.com/feed') + return response.json() +} +``` + +### Computed data after dynamic checks + +Cache expensive computations that occur after dynamic security or feature checks: + +```tsx filename="app/reports/page.tsx" +import { connection } from 'next/server' +import { cacheLife } from 'next/cache' + +export default async function ReportsPage() { + // Dynamic security check + await connection() + + const report = await generateReport() + + return +} + +async function generateReport() { + 'use cache: remote' + cacheLife({ expire: 3600 }) // 1 hour + + // This expensive computation is cached and shared across all authorized users, + // avoiding repeated calculations + const data = await db.transactions.findMany() + + return { + totalRevenue: calculateRevenue(data), + topProducts: analyzeProducts(data), + trends: calculateTrends(data), + } +} +``` + +### Mixed caching strategies + +Combine static, remote, and private caching for optimal performance: + +```tsx filename="app/product/[id]/page.tsx" +import { Suspense } from 'react' +import { connection } from 'next/server' +import { cookies } from 'next/headers' +import { cacheLife, cacheTag } from 'next/cache' + +// Static product data - prerendered at build time +async function getProduct(id: string) { + 'use cache' + cacheTag(`product-${id}`) + + // This is cached at build time and shared across all users + return db.products.find({ where: { id } }) +} + +// Shared pricing data - cached at runtime in remote handler +async function getProductPrice(id: string) { + 'use cache: remote' + cacheTag(`product-price-${id}`) + cacheLife({ expire: 300 }) // 5 minutes + + // This is cached at runtime and shared across all users + return db.products.getPrice({ where: { id } }) +} + +// User-specific recommendations - private cache per user +async function getRecommendations(productId: string) { + 'use cache: private' + cacheLife({ expire: 60 }) // 1 minute + + const sessionId = (await cookies()).get('session-id')?.value + + // This is cached per-user and never shared + return db.recommendations.findMany({ + where: { productId, sessionId }, + }) +} + +export default async function ProductPage({ params }) { + const { id } = await params + + // Static product data + const product = await getProduct(id) + + return ( +
+ + + {/* Dynamic shared price */} + }> + + + + {/* Dynamic personalized recommendations */} + }> + + +
+ ) +} + +function ProductDetails({ product }) { + return ( +
+

{product.name}

+

{product.description}

+
+ ) +} + +async function ProductPriceComponent({ productId }) { + // Make this component dynamic + await connection() + + const price = await getProductPrice(productId) + return
Price: ${price}
+} + +async function ProductRecommendations({ productId }) { + const recommendations = await getRecommendations(productId) + return +} + +function PriceSkeleton() { + return
Loading price...
+} + +function RecommendationsSkeleton() { + return
Loading recommendations...
+} + +function RecommendationsList({ items }) { + return ( +
    + {items.map((item) => ( +
  • {item.name}
  • + ))} +
+ ) +} +``` + +> **Good to know**: +> +> - Remote caches are stored in server-side cache handlers and shared across all users +> - Remote caches work in dynamic contexts where regular [`use cache`](/docs/app/api-reference/directives/use-cache) would fail +> - Use [`cacheTag()`](/docs/app/api-reference/functions/cacheTag) and [`revalidateTag()`](/docs/app/api-reference/functions/revalidateTag) to invalidate remote caches on-demand +> - Use [`cacheLife()`](/docs/app/api-reference/functions/cacheLife) to configure cache expiration +> - For user-specific data, use [`'use cache: private'`](/docs/app/api-reference/directives/use-cache-private) instead of `'use cache: remote'` +> - Remote caches reduce origin load by storing computed or fetched data server-side + +## Platform Support + +| Deployment Option | Supported | +| ------------------------------------------------------------------- | --------- | +| [Node.js server](/docs/app/getting-started/deploying#nodejs-server) | Yes | +| [Docker container](/docs/app/getting-started/deploying#docker) | Yes | +| [Static export](/docs/app/getting-started/deploying#static-export) | No | +| [Adapters](/docs/app/getting-started/deploying#adapters) | Yes | + +## Version History + +| Version | Changes | +| --------- | ------------------------------------------------------------ | +| `v16.0.0` | `'use cache: remote'` introduced as an experimental feature. |