Skip to content
Lantorian

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.

/fr/shoppage complete
Categoriesfrom cache
Product listfrom cache
Cartrendered at request time
Cached blocks show up instantly; the cart, specific to each user, can never be cached.

Where to load data

NeedTool
Display data in a pageAsync Server Component calling a function from queries.ts
Create, update, deleteServer Action in actions.ts, validated with Zod
Webhook, endpoint called by a third partyRoute Handler app/api/…/route.ts
Real time, polling, infinite scrollTanStack 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

page.tsx
// Waterfall: 3 sequential requests (300 + 250 + 400 ms)
const customer = await getCustomer(id);
const invoices = await getInvoices(id);
const stats = await getStats(id);

Do

page.tsx
// 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.

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,    // explicit caching with 'use cache'
  partialPrefetching: true, // Instant Navigations (16.3)
};

export default nextConfig;

After a change

A mutation must tell Next.js which data is stale. Pick the API based on what the user expects.

APIWhen
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.
src/features/shop/actions.ts
'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.

src/proxy.ts
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'.

src/components/price-tag.tsx
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>;
}