Next.js10 min read
Data and caching
Since Next.js 16, everything is dynamic by default and caching is explicit. You decide, component by component, what can be cached and what must be fresh on every request.
The Cache Components model
A component marked 'use cache' is rendered ahead of time and becomes part of the static shell. A component that reads request data (cookies(), headers()) stays dynamic and streams in inside a Suspense. Flip the switches, then simulate a navigation.
- Categories
'use cache' cacheLife('days')Rarely changes: cached for a day.
- Product list
'use cache' cacheTag('products')Tagged: invalidated as soon as a product changes.
- Cart
await cookies() <Suspense>
Reads the session cookie: always dynamic, inside Suspense.
Where to load data
| Need | Tool |
|---|---|
| Display data in a page | Async Server Component calling a function from queries.ts |
| Create, update, delete | Server Action in actions.ts, validated with Zod |
| Webhook, endpoint called by a third party | Route Handler app/api/…/route.ts |
| Real time, polling, infinite scroll | TanStack Query in a Client Component, rarely |
No request waterfalls
When several pieces of data are independent, fire them in parallel with Promise.all. If only one is slow, isolate it in its own Suspense.
Avoid
// Waterfall: 3 sequential requests (300 + 250 + 400 ms)
const customer = await getCustomer(id);
const invoices = await getInvoices(id);
const stats = await getStats(id);Do
// In parallel: as slow as the slowest request (400 ms)
const [customer, invoices, stats] = await Promise.all([
getCustomer(id),
getInvoices(id),
getStats(id),
]);Caching
'use cache' applies to a function or a component. cacheLife sets the duration ('hours', 'days', 'max'), cacheTag names it so you can invalidate it later.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true, // explicit caching with 'use cache'
partialPrefetching: true, // Instant Navigations (16.3)
};
export default nextConfig;import { cacheLife, cacheTag } from 'next/cache';
import { apiFetch } from '@/lib/api-client';
export async function getCategories() {
'use cache';
cacheLife('days');
return apiFetch('/v1/categories');
}
export async function getProducts(categoryId: number) {
'use cache';
cacheLife('hours');
cacheTag('products', `category-${categoryId}`);
return apiFetch(`/v1/categories/${categoryId}/products`);
}import { cookies } from 'next/headers';
import { Suspense } from 'react';
export default async function ShopPage({ params }: PageProps<'/[locale]/shop/[category]'>) {
const { category } = await params;
return (
<>
<CategoryNav /> {/* cached: in the static shell */}
<ProductGrid categoryId={Number(category)} /> {/* cached with a tag */}
<Suspense fallback={<CartSkeleton />}>
<Cart /> {/* dynamic: reads cookies */}
</Suspense>
</>
);
}
async function Cart() {
const session = (await cookies()).get('session')?.value; // request-time data
const cart = await getCart(session);
return <CartSummary cart={cart} />;
}After a change
A mutation must tell Next.js which data is stale. Pick the API based on what the user expects.
| API | When |
|---|---|
updateTag(tag) | In a Server Action: the user must see their change immediately. |
revalidateTag(tag, 'max') | Content that may lag slightly: the old version is served while it recomputes. |
refresh() | Refresh only uncached data (notification counter). |
router.refresh() | Client side, reload the current route's Server Components. |
'use server';
import { refresh, revalidateTag, updateTag } from 'next/cache';
export async function renameProduct(id: number, name: string) {
await apiFetch(`/v1/products/${id}`, { method: 'PATCH', body: { name } });
updateTag('products'); // the user sees their change right away
}
export async function importCatalog() {
await apiFetch('/v1/catalog/import', { method: 'POST' });
revalidateTag('products', 'max'); // stale-while-revalidate for everyone else
}
export async function markNotificationRead(id: number) {
await apiFetch(`/v1/notifications/${id}/read`, { method: 'POST' });
refresh(); // re-render uncached data only (header badge)
}proxy.ts
Next.js 16 renames middleware.ts to proxy.ts. It handles locale detection and an optimistic session check. Real authorization stays in Laravel.
import { NextResponse, type NextRequest } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
const intl = createIntlMiddleware(routing);
const protectedPaths = /^\/(fr|en)\/(dashboard|invoices)/;
export default function proxy(request: NextRequest) {
// Optimistic check only: real authorization happens in Laravel
const hasSession = request.cookies.has('laravel_session');
if (protectedPaths.test(request.nextUrl.pathname) && !hasSession) {
return NextResponse.redirect(new URL('/fr/login', request.url));
}
return intl(request);
}
export const config = { matcher: '/((?!api|_next|_vercel|.*\\..*).*)' };Reading the locale anywhere
New in Next.js 16.3: next/root-params exposes the [locale] param to any Server Component, even inside 'use cache'.
import { locale } from 'next/root-params'; // Next.js 16.3
// Any Server Component, however deep, without prop drilling
export async function PriceTag({ amount }: { amount: number }) {
const current = await locale();
return <span>{new Intl.NumberFormat(current, { style: 'currency', currency: 'EUR' }).format(amount)}</span>;
}