Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
import { CachedBox } from '@/components/scenarioBox';

// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss
// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole.
Expand All @@ -15,11 +16,14 @@ export default async function CachedMidLayout({
const { id } = await params;
await new Promise(resolve => setTimeout(resolve, 100));
return (
<div data-testid="cached-mid-layout">
<p id="cached-layout-stamp">
{id}:{Date.now()}
</p>
{children}
</div>
<main>
<h1>Cached layout, dynamic page</h1>
<CachedBox label="CachedMidLayout · use cache · hours · keyed by [id]">
<p id="cached-layout-stamp">
{id}:{Date.now()}
</p>
{children}
</CachedBox>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
import { DynamicBox } from '@/components/scenarioBox';

export default async function Page() {
await headers();
return <p id="dynamic-leaf">Dynamic leaf: {Date.now()}</p>;
return (
<DynamicBox label="page · awaits headers">
<p id="dynamic-leaf">Dynamic leaf: {Date.now()}</p>
</DynamicBox>
);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { cacheLife } from 'next/cache';
import { CachedBox } from '@/components/scenarioBox';

async function CachedLeaf({ id }: { id: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
<p id="cached-leaf">
{id}:{Date.now()}
</p>
<CachedBox label="CachedLeaf · use cache · hours">
<p id="cached-leaf">
{id}:{Date.now()}
</p>
</CachedBox>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type { ReactNode } from 'react';
import { headers } from 'next/headers';
import { DynamicBox } from '@/components/scenarioBox';

export default async function DynamicLayout({ children }: { children: ReactNode }) {
await headers();
return <div data-testid="dynamic-layout">{children}</div>;
return (
<main>
<h1>Dynamic layout, cached leaf</h1>
<DynamicBox label="DynamicLayout · awaits headers">
<p>request-time: {Date.now()}</p>
{children}
</DynamicBox>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
import { CachedBox } from '@/components/scenarioBox';

// Hard-expires after 2s while the page's cached component lives for hours, so one request can
// refill the layout while hitting the component: two cached levels with different origin traces.
Expand All @@ -14,11 +15,14 @@ export default async function ShortLivedLayout({
cacheLife({ revalidate: 1, expire: 2 });
const { id } = await params;
return (
<div data-testid="short-lived-layout">
<p id="layout-stamp">
{id}:{Date.now()}
</p>
{children}
</div>
<main>
<h1>Mixed lifetimes</h1>
<CachedBox label="ShortLivedLayout · use cache · expires after 2s · keyed by [id]">
<p id="layout-stamp">
{id}:{Date.now()}
</p>
{children}
</CachedBox>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { headers } from 'next/headers';
import { cacheLife } from 'next/cache';
import { CachedBox, DynamicBox } from '@/components/scenarioBox';

async function LongLivedComponent({ id }: { id: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
<p id="component-stamp">
{id}:{Date.now()}
</p>
<CachedBox label="LongLivedComponent · use cache · hours">
<p id="component-stamp">
{id}:{Date.now()}
</p>
</CachedBox>
);
}

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
await headers();
return <LongLivedComponent id={id} />;
return (
<DynamicBox label="page · awaits headers">
<p>request-time: {Date.now()}</p>
<LongLivedComponent id={id} />
</DynamicBox>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
import { DynamicBox } from '@/components/scenarioBox';

export default async function Page() {
await headers();
return <p id="route-a">Route a: {Date.now()}</p>;
return (
<DynamicBox label="route a · awaits headers">
<p id="route-a">Route a: {Date.now()}</p>
</DynamicBox>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { headers } from 'next/headers';
import { DynamicBox } from '@/components/scenarioBox';

export default async function Page() {
await headers();
return <p id="route-b">Route b: {Date.now()}</p>;
return (
<DynamicBox label="route b · awaits headers">
<p id="route-b">Route b: {Date.now()}</p>
</DynamicBox>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { cacheLife } from 'next/cache';
import { CachedBox } from '@/components/scenarioBox';

// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below.
export default async function SharedLayout({
Expand All @@ -14,11 +15,14 @@ export default async function SharedLayout({
const { id } = await params;
await new Promise(resolve => setTimeout(resolve, 100));
return (
<div data-testid="shared-layout">
<p id="shared-layout-stamp">
{id}:{Date.now()}
</p>
{children}
</div>
<main>
<h1>Shared layout across sibling routes</h1>
<CachedBox label="SharedLayout · use cache · hours · shared by /a and /b">
<p id="shared-layout-stamp">
{id}:{Date.now()}
</p>
{children}
</CachedBox>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
import { CachedBox, DynamicBox } from '@/components/scenarioBox';

async function CachedSection({ id, label }: { id: string; label: string }) {
'use cache';
cacheLife('hours');
await new Promise(resolve => setTimeout(resolve, 100));
return (
<section id={`cached-${label}`}>
{label}:{id}:{Date.now()}
</section>
<CachedBox label={`CachedSection "${label}" · use cache · hours`}>
<section id={`cached-${label}`}>
{label}:{id}:{Date.now()}
</section>
</CachedBox>
);
}

Expand All @@ -18,17 +21,21 @@ async function DynamicContent({ searchParams }: { searchParams: Promise<{ id?: s
// separate entries (the props are part of the cache key).
const { id = 'default-id' } = await searchParams;
return (
<>
<DynamicBox label="dynamic hole · awaits searchParams">
<p>request-time: {Date.now()}</p>
<CachedSection id={id} label="first" />
<CachedSection id={id} label="second" />
</>
</DynamicBox>
);
}

export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent searchParams={searchParams} />
</Suspense>
<main>
<h1>Cached sibling components</h1>
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent searchParams={searchParams} />
</Suspense>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<body style={{ fontFamily: 'system-ui, sans-serif', maxWidth: 760, margin: '0 auto', padding: '0 16px' }}>
<header style={{ borderBottom: '1px solid #ddd', padding: '10px 0', marginBottom: 16 }}>
<a href="/">← Cache scenarios</a>
</header>
{children}
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Suspense } from 'react';
import { cacheLife } from 'next/cache';
import { CachedBox, DynamicBox } from '@/components/scenarioBox';

async function getSlowChangingValue(id: string): Promise<{ id: string; createdAt: number }> {
'use cache';
Expand All @@ -14,20 +15,34 @@ async function ShortLivedSection({ id }: { id: string }) {
// delayed request refills the component and reads the nested entry as a hit inside that fill.
cacheLife({ revalidate: 1, expire: 2 });
const nested = await getSlowChangingValue(id);
return <div id="nested-data">{JSON.stringify({ ...nested, renderedAt: Date.now() })}</div>;
return (
<CachedBox label="ShortLivedSection · use cache · expires after 2s">
<CachedBox label="getSlowChangingValue · use cache · hours">
<div id="nested-data">{JSON.stringify({ ...nested, renderedAt: Date.now() })}</div>
</CachedBox>
</CachedBox>
);
}

async function DynamicContent({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
// Awaiting searchParams makes this hole dynamic, so every request renders it and consults the
// cache handler instead of serving prerendered output.
const { id = 'default-id' } = await searchParams;
return <ShortLivedSection id={id} />;
return (
<DynamicBox label="dynamic hole · awaits searchParams">
<p>request-time: {Date.now()}</p>
<ShortLivedSection id={id} />
</DynamicBox>
);
}

export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent searchParams={searchParams} />
</Suspense>
<main>
<h1>Nested caches with different lifetimes</h1>
<Suspense fallback={<div>Loading...</div>}>
<DynamicContent searchParams={searchParams} />
</Suspense>
</main>
);
}
Loading
Loading