|
|
|
@@ -0,0 +1,216 @@
|
|
|
|
|
---
|
|
|
|
|
title: Incremental Static Regeneration with Cache Components
|
|
|
|
|
description: Learn how to prerender a subset of dynamic routes, serve fallback shells for the rest, and upgrade them after the first visit.
|
|
|
|
|
nav_title: ISR with Cache Components
|
|
|
|
|
version: experimental
|
|
|
|
|
related:
|
|
|
|
|
links:
|
|
|
|
|
- app/api-reference/config/next-config-js/cacheComponents
|
|
|
|
|
- app/api-reference/functions/generate-static-params
|
|
|
|
|
- app/api-reference/file-conventions/loading
|
|
|
|
|
- app/getting-started/caching
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[Incremental Static Regeneration (ISR)](/docs/app/glossary#incremental-static-regeneration-isr) with [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) lets you:
|
|
|
|
|
|
|
|
|
|
- Prerender a subset of your dynamic routes at build time with [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)
|
|
|
|
|
- For routes not included in that subset, prerender a **fallback shell**: the static UI Next.js can produce by treating params as runtime data
|
|
|
|
|
- Serve something instantly for every route: either the prerendered page or the fallback shell
|
|
|
|
|
- Progressively improve what visitors see for a given route as param values become known from real traffic
|
|
|
|
|
|
|
|
|
|
If you have used [ISR](/docs/app/guides/incremental-static-regeneration) or [`fallback: true`](https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-true) in the Pages Router, this is the Cache Components equivalent.
|
|
|
|
|
|
|
|
|
|
Make sure [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) is enabled in your project:
|
|
|
|
|
|
|
|
|
|
```ts filename="next.config.ts" highlight={4}
|
|
|
|
|
import type { NextConfig } from 'next'
|
|
|
|
|
|
|
|
|
|
const nextConfig: NextConfig = {
|
|
|
|
|
cacheComponents: true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default nextConfig
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Example
|
|
|
|
|
|
|
|
|
|
We'll build a product catalog with category layouts and product detail pages using the [Recipe API](https://next-recipe-api.vercel.dev/). You can find the resources used in this example here:
|
|
|
|
|
|
|
|
|
|
- [Demo](https://partial-fallbacks.labs.vercel.dev/)
|
|
|
|
|
- [Code](https://github.com/vercel-labs/partial-fallbacks)
|
|
|
|
|
|
|
|
|
|
### Prepare your routes
|
|
|
|
|
|
|
|
|
|
Use [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) to define which param values to prerender. When rendering these routes, the param values are known at build time. The prerender process builds a static shell until it hits runtime APIs or uncached data. See [how rendering works](/docs/app/getting-started/caching#how-rendering-works) for more details.
|
|
|
|
|
|
|
|
|
|
The category layout prerenders two categories:
|
|
|
|
|
|
|
|
|
|
```tsx filename="app/[category]/layout.tsx" highlight={5-8}
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { Suspense } from 'react'
|
|
|
|
|
import { getCategory, getTopCategories } from '../lib/data'
|
|
|
|
|
|
|
|
|
|
export async function generateStaticParams() {
|
|
|
|
|
const categories = await getTopCategories()
|
|
|
|
|
return categories.map((c) => ({ category: c.slug }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function CategoryHeader({
|
|
|
|
|
params,
|
|
|
|
|
}: Pick<LayoutProps<'/[category]'>, 'params'>) {
|
|
|
|
|
const { category } = await params
|
|
|
|
|
const data = await getCategory(category)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<Link href="/">← All categories</Link>
|
|
|
|
|
<h1>{data?.name ?? 'Category'}</h1>
|
|
|
|
|
{data?.description && <p>{data.description}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function CategoryLayout(props: LayoutProps<'/[category]'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<Suspense fallback={<div>Loading...</div>}>
|
|
|
|
|
<CategoryHeader params={props.params} />
|
|
|
|
|
</Suspense>
|
|
|
|
|
{props.children}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Notice that `CategoryLayout` does not `await props.params` itself. Instead, it passes the `params` promise to `CategoryHeader` inside `<Suspense>`. The `await` happens inside the boundary, so for unknown categories Next.js can still generate a static shell with the fallback UI.
|
|
|
|
|
|
|
|
|
|
The product page prerenders one product per category. `generateStaticParams` receives the parent `category` param:
|
|
|
|
|
|
|
|
|
|
```tsx filename="app/[category]/[product]/page.tsx" highlight={5-12}
|
|
|
|
|
import { Suspense } from 'react'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { getProduct, getPopularProducts } from '../../lib/data'
|
|
|
|
|
|
|
|
|
|
export async function generateStaticParams({
|
|
|
|
|
params,
|
|
|
|
|
}: {
|
|
|
|
|
params: { category: string }
|
|
|
|
|
}) {
|
|
|
|
|
const products = await getPopularProducts(params.category)
|
|
|
|
|
return products.map((p) => ({ product: p.slug }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ProductDetails(props: PageProps<'/[category]/[product]'>) {
|
|
|
|
|
const { category, product } = await props.params
|
|
|
|
|
const data = await getProduct(category, product)
|
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
|
|
return <p>Product not found.</p>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Link href={`/${category}`}>← Back to products</Link>
|
|
|
|
|
<h2>{data.name}</h2>
|
|
|
|
|
<p>${data.price}</p>
|
|
|
|
|
<p>{data.description}</p>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ProductPage(props: PageProps<'/[category]/[product]'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<Suspense fallback={<div>Loading product...</div>}>
|
|
|
|
|
<ProductDetails {...props} />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> **Good to know:** You can also use [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) files instead of `<Suspense>` in JSX. `loading.tsx` puts the boundary at the segment edge, while inline `<Suspense>` lets you place it anywhere in the tree.
|
|
|
|
|
|
|
|
|
|
The data helpers use `'use cache'` at the module level so all exported functions are cached. This lets their results be included in the static shell:
|
|
|
|
|
|
|
|
|
|
```ts filename="app/lib/data.ts" highlight={1}
|
|
|
|
|
'use cache'
|
|
|
|
|
|
|
|
|
|
const API = 'https://next-recipe-api.vercel.dev'
|
|
|
|
|
|
|
|
|
|
export async function getCategory(slug: string) {
|
|
|
|
|
const res = await fetch(`${API}/categories/${slug}`)
|
|
|
|
|
if (!res.ok) return null
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getProduct(category: string, slug: string) {
|
|
|
|
|
const res = await fetch(`${API}/products/${category}/${slug}`)
|
|
|
|
|
if (!res.ok) return null
|
|
|
|
|
return res.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getTopCategories() {
|
|
|
|
|
const res = await fetch(`${API}/categories`)
|
|
|
|
|
const categories = await res.json()
|
|
|
|
|
return categories.slice(0, 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getPopularProducts(category: string) {
|
|
|
|
|
const res = await fetch(`${API}/products?category=${category}`)
|
|
|
|
|
const products = await res.json()
|
|
|
|
|
return products.slice(0, 1)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If your components access runtime APIs like `cookies` or `headers`, wrap them in `<Suspense>`. Their fallback UI is included in the static shell instead.
|
|
|
|
|
|
|
|
|
|
### At build time
|
|
|
|
|
|
|
|
|
|
When you run `next build`, Next.js prerenders the layout for each known category (`tops`, `shorts`), plus one render where `await params` suspends, producing the `[category]` shell.
|
|
|
|
|
|
|
|
|
|
It also prerenders the page for each known product under each category (`tee` under `tops`, `joggers` under `shorts`), plus one render where `await params` suspends, producing the `[product]` shell.
|
|
|
|
|
|
|
|
|
|
These are combined into:
|
|
|
|
|
|
|
|
|
|
- `/tops/tee`, `/shorts/joggers`: fully static pages, both params known
|
|
|
|
|
- `/tops/[product]`, `/shorts/[product]`: category header rendered, product shows fallback
|
|
|
|
|
- `/[category]/[product]`: both show fallback
|
|
|
|
|
|
|
|
|
|
### At runtime
|
|
|
|
|
|
|
|
|
|
A visitor navigates to `/tops/tee`. Both params were prerendered. They get a fully static page.
|
|
|
|
|
|
|
|
|
|
The first visit to `/tops/overshirt`. The product is unknown, but the category `tops` was prerendered. Next.js serves the `/tops/[product]` shell with the category header already rendered. The product streams in.
|
|
|
|
|
|
|
|
|
|
The first visit to `/shoes/basketball-shoes`. The category `shoes` was not prerendered. Next.js serves the generic `/[category]/[product]` shell. Both the category and the product stream in.
|
|
|
|
|
|
|
|
|
|
After the first visit, Next.js renders these routes in the background with the now-known params. The next visitor to the same URLs gets a more specific result.
|
|
|
|
|
|
|
|
|
|
### What the upgrade produces
|
|
|
|
|
|
|
|
|
|
After the first visit, Next.js renders the page in the background with the known params and tries to push the static boundary as far down the component tree as possible:
|
|
|
|
|
|
|
|
|
|
- If every data access is cached and all params are resolved, the upgrade produces a **fully static page**.
|
|
|
|
|
- If some data is uncached or runtime APIs (`cookies`, `headers`) are accessed behind `<Suspense>` fallbacks, the upgrade produces a **more specific shell** with the params resolved but the dynamic parts still streaming.
|
|
|
|
|
- Params are resolved in route order. A param without `generateStaticParams` blocks all subsequent params from upgrading.
|
|
|
|
|
|
|
|
|
|
> **Good to know**: Prefetching also triggers upgrades. When a [`<Link>`](/docs/app/api-reference/components/link) enters the viewport or [`router.prefetch`](/docs/app/api-reference/functions/use-router) is called, Next.js can upgrade the shell in the background, so the next visitor gets the more specific version even before anyone actually navigates to the page.
|
|
|
|
|
|
|
|
|
|
> **Good to know**: When [self-hosting](/docs/app/guides/self-hosting#caching-and-isr) with a custom [`cacheHandler`](/docs/app/api-reference/config/next-config-js/incrementalCacheHandlerPath) for ISR, you can keep it alongside [`cacheHandlers`](/docs/app/api-reference/config/next-config-js/cacheHandlers) for `'use cache'`. They serve different layers and coexist.
|
|
|
|
|
|
|
|
|
|
## Coming from the Pages Router
|
|
|
|
|
|
|
|
|
|
If you are migrating from the Pages Router:
|
|
|
|
|
|
|
|
|
|
- `fallback: true` in `getStaticPaths` is now the default behavior with `cacheComponents`. Visitors get a `<Suspense>` fallback instantly and content streams in.
|
|
|
|
|
- `router.isFallback` is not needed. The prerendering step generates a static shell, and you can grow it further with `'use cache'`.
|
|
|
|
|
- `getStaticProps` with `revalidate` maps to `'use cache'` with [`cacheLife`](/docs/app/api-reference/functions/cacheLife).
|
|
|
|
|
- `getStaticPaths` maps to [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params).
|
|
|
|
|
|
|
|
|
|
## Next steps
|
|
|
|
|
|
|
|
|
|
- [Caching with Cache Components](/docs/app/getting-started/caching) for the full caching model
|
|
|
|
|
- [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) for controlling which param combinations are prerendered
|
|
|
|
|
- [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) for providing skeleton UI in fallback shells
|
|
|
|
|
- [Streaming](/docs/app/guides/streaming) to learn how to progressively render UI as data becomes available
|