From 1dd4017d7e17e65ef640c5955bc1010cf13f003b Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:43:52 +0200 Subject: [PATCH] test(nextjs): Add UI components for cached/dynamic content --- .../cached-mid-layout/[id]/layout.tsx | 16 ++- .../cached-mid-layout/[id]/page.tsx | 7 +- .../[id]/layout-cached-leaf/page.tsx | 9 +- .../dynamic-layouts/[id]/layout.tsx | 11 +- .../mixed-lifetimes/[id]/layout.tsx | 16 ++- .../mixed-lifetimes/[id]/page.tsx | 16 ++- .../shared-layout/[id]/a/page.tsx | 7 +- .../shared-layout/[id]/b/page.tsx | 7 +- .../shared-layout/[id]/layout.tsx | 16 ++- .../app/cached-sibling-components/page.tsx | 23 ++-- .../nextjs-16-cacheComponents/app/layout.tsx | 7 +- .../app/nested-caches/page.tsx | 25 +++- .../nextjs-16-cacheComponents/app/page.tsx | 119 +++++++++++++++++- .../app/use-cache-page/page.tsx | 19 ++- .../components/scenarioBox.tsx | 34 +++++ .../cached-mid-layout/[id]/layout.tsx | 16 ++- .../cached-mid-layout/[id]/page.tsx | 7 +- .../[id]/layout-cached-leaf/page.tsx | 9 +- .../dynamic-layouts/[id]/layout.tsx | 11 +- .../mixed-lifetimes/[id]/layout.tsx | 16 ++- .../mixed-lifetimes/[id]/page.tsx | 16 ++- .../shared-layout/[id]/a/page.tsx | 7 +- .../shared-layout/[id]/b/page.tsx | 7 +- .../shared-layout/[id]/layout.tsx | 16 ++- .../app/cached-sibling-components/page.tsx | 23 ++-- .../app/layout.tsx | 7 +- .../app/nested-caches/page.tsx | 25 +++- .../app/page.tsx | 114 ++++++++++++++++- .../components/scenarioBox.tsx | 34 +++++ 29 files changed, 548 insertions(+), 92 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx index a61f15ee7e3f..ed7d8d1716d8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx @@ -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. @@ -15,11 +16,14 @@ export default async function CachedMidLayout({ const { id } = await params; await new Promise(resolve => setTimeout(resolve, 100)); return ( -
-

- {id}:{Date.now()} -

- {children} -
+
+

Cached layout, dynamic page

+ +

+ {id}:{Date.now()} +

+ {children} +
+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx index 94196e6e5ee8..1edb269289ba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx @@ -1,6 +1,11 @@ import { headers } from 'next/headers'; +import { DynamicBox } from '@/components/scenarioBox'; export default async function Page() { await headers(); - return

Dynamic leaf: {Date.now()}

; + return ( + +

Dynamic leaf: {Date.now()}

+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx index 8e7de36d156a..494dee2483e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx @@ -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 ( -

- {id}:{Date.now()} -

+ +

+ {id}:{Date.now()} +

+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx index 412587fd8f33..4b9b4cffc9bf 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx @@ -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
{children}
; + return ( +
+

Dynamic layout, cached leaf

+ +

request-time: {Date.now()}

+ {children} +
+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx index 2b0b77b6e3a9..7920ef335dc2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx @@ -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. @@ -14,11 +15,14 @@ export default async function ShortLivedLayout({ cacheLife({ revalidate: 1, expire: 2 }); const { id } = await params; return ( -
-

- {id}:{Date.now()} -

- {children} -
+
+

Mixed lifetimes

+ +

+ {id}:{Date.now()} +

+ {children} +
+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx index 18913cbe9790..c1c0e31494f6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx @@ -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 ( -

- {id}:{Date.now()} -

+ +

+ {id}:{Date.now()} +

+
); } export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; await headers(); - return ; + return ( + +

request-time: {Date.now()}

+ +
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx index ec8d5e21d947..606b9f01a2b2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx @@ -1,6 +1,11 @@ import { headers } from 'next/headers'; +import { DynamicBox } from '@/components/scenarioBox'; export default async function Page() { await headers(); - return

Route a: {Date.now()}

; + return ( + +

Route a: {Date.now()}

+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx index 9a1b84b2ca28..18bff4a0ed35 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx @@ -1,6 +1,11 @@ import { headers } from 'next/headers'; +import { DynamicBox } from '@/components/scenarioBox'; export default async function Page() { await headers(); - return

Route b: {Date.now()}

; + return ( + +

Route b: {Date.now()}

+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx index 220fdcf92ca8..16fa37dbd71e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx @@ -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({ @@ -14,11 +15,14 @@ export default async function SharedLayout({ const { id } = await params; await new Promise(resolve => setTimeout(resolve, 100)); return ( -
-

- {id}:{Date.now()} -

- {children} -
+
+

Shared layout across sibling routes

+ +

+ {id}:{Date.now()} +

+ {children} +
+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx index c8b8858f7785..c3e4050539bb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/cached-sibling-components/page.tsx @@ -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 ( -
- {label}:{id}:{Date.now()} -
+ +
+ {label}:{id}:{Date.now()} +
+
); } @@ -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 ( - <> + +

request-time: {Date.now()}

- +
); } export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) { return ( - Loading...}> - - +
+

Cached sibling components

+ Loading...}> + + +
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx index c8f9cee0b787..5ffd72655346 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/layout.tsx @@ -1,7 +1,12 @@ export default function Layout({ children }: { children: React.ReactNode }) { return ( - {children} + +
+ ← Cache scenarios +
+ {children} + ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx index 481d7e5e46e6..1115c4054b3f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/nested-caches/page.tsx @@ -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'; @@ -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
{JSON.stringify({ ...nested, renderedAt: Date.now() })}
; + return ( + + +
{JSON.stringify({ ...nested, renderedAt: Date.now() })}
+
+
+ ); } 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 ; + return ( + +

request-time: {Date.now()}

+ +
+ ); } export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) { return ( - Loading...}> - - +
+

Nested caches with different lifetimes

+ Loading...}> + + +
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx index 433db8283beb..bea2cf1d875f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/page.tsx @@ -1,3 +1,120 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +type Scenario = { name: string; href: (id: string) => string; description: string }; + +const groups: { title: string; scenarios: Scenario[] }[] = [ + { + title: 'Route handlers (JSON)', + scenarios: [ + { + name: '/api/use-cache', + href: id => `/api/use-cache?id=${id}`, + description: 'cached function that lives for hours. The second request with the same id is a hit.', + }, + { + name: '/api/use-cache-expiring', + href: id => `/api/use-cache-expiring?id=${id}`, + description: 'the entry expires after 2 seconds. Request again after 3 seconds for a refill.', + }, + { + name: '/api/use-cache-swr', + href: id => `/api/use-cache-swr?id=${id}`, + description: + 'the entry goes stale after 2 seconds. A stale read serves the old value and refills in the background.', + }, + ], + }, + { + title: 'Rendered pages', + scenarios: [ + { + name: '/use-cache-page', + href: id => `/use-cache-page?id=${id}`, + description: 'a cached data function inside a dynamic hole.', + }, + { + name: '/cached-sibling-components', + href: id => `/cached-sibling-components?id=${id}`, + description: 'two cached sibling components. Each section is its own cache entry.', + }, + { + name: '/nested-caches', + href: id => `/nested-caches?id=${id}`, + description: + 'a cached component that expires after 2 seconds and calls a cached function that lives for hours.', + }, + ], + }, + { + title: 'Nested layouts', + scenarios: [ + { + name: '/cached-mid-layout/[id]', + href: id => `/cached-mid-layout/${id}`, + description: 'a cached layout above a dynamic page.', + }, + { + name: '/dynamic-layouts/[id]/layout-cached-leaf', + href: id => `/dynamic-layouts/${id}/layout-cached-leaf`, + description: 'dynamic layouts above a cached leaf component.', + }, + { + name: '/mixed-lifetimes/[id]', + href: id => `/mixed-lifetimes/${id}`, + description: 'a cached layout that expires after 2 seconds around a cached component that lives for hours.', + }, + { + name: '/shared-layout/[id]/a', + href: id => `/shared-layout/${id}/a`, + description: 'route a fills the layout entry that routes a and b share.', + }, + { + name: '/shared-layout/[id]/b', + href: id => `/shared-layout/${id}/b`, + description: 'route b hits the layout entry that route a filled.', + }, + ], + }, + { + title: 'Pageload', + scenarios: [ + { + name: '/pageload-tracing', + href: () => '/pageload-tracing', + description: 'a prerendered shell with a dynamic hole. Check the pageload and server trace connection here.', + }, + ], + }, +]; + +// The links are plain `` elements so that every click is a full document request with its own +// server trace. `` would navigate through the client-side router instead. export default function Page() { - return

Next 16 CacheComponents test app

; + const [id, setId] = useState(''); + // Generated after hydration so the prerendered shell does not bake in one fixed id. + useEffect(() => setId(crypto.randomUUID()), []); + + return ( +
+

Next 16 Cache Components scenarios

+

+ Every link carries a random id that becomes part of the cache key. The first click fills the cache. A second + click on the same link, or a reload of the target page, hits it. Reload this page to get fresh ids. +

+ {groups.map(group => ( +
+

{group.title}

+
+
+ ))} +
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx index 1e41b4436e20..3450928fe090 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/use-cache-page/page.tsx @@ -1,5 +1,6 @@ import { Suspense } from 'react'; import { cacheLife } from 'next/cache'; +import { CachedBox, DynamicBox } from '@/components/scenarioBox'; async function getCachedPageData(id: string): Promise<{ id: string; createdAt: number }> { 'use cache'; @@ -10,9 +11,12 @@ async function getCachedPageData(id: string): Promise<{ id: string; createdAt: n export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) { return ( - Loading...}> - - +
+

Cached data function in a page

+ Loading...}> + + +
); } @@ -21,5 +25,12 @@ async function CachedContent({ searchParams }: { searchParams: Promise<{ id?: st // cache handler instead of serving prerendered output. const { id = 'default-id' } = await searchParams; const data = await getCachedPageData(id); - return
{JSON.stringify(data)}
; + return ( + +

request-time: {Date.now()}

+ +
{JSON.stringify(data)}
+
+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx new file mode 100644 index 000000000000..cf2d318d64cc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/components/scenarioBox.tsx @@ -0,0 +1,34 @@ +import type { CSSProperties, ReactNode } from 'react'; + +const box: CSSProperties = { + padding: '8px 12px', + margin: '12px 0', + borderRadius: 8, +}; + +const badge: CSSProperties = { + display: 'inline-block', + fontSize: 12, + fontWeight: 600, + color: '#fff', + padding: '1px 8px', + borderRadius: 4, +}; + +export function CachedBox({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ Cached · {label} + {children} +
+ ); +} + +export function DynamicBox({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ Dynamic · {label} + {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx index a61f15ee7e3f..ed7d8d1716d8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx @@ -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. @@ -15,11 +16,14 @@ export default async function CachedMidLayout({ const { id } = await params; await new Promise(resolve => setTimeout(resolve, 100)); return ( -
-

- {id}:{Date.now()} -

- {children} -
+
+

Cached layout, dynamic page

+ +

+ {id}:{Date.now()} +

+ {children} +
+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx index 94196e6e5ee8..1edb269289ba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/cached-mid-layout/[id]/page.tsx @@ -1,6 +1,11 @@ import { headers } from 'next/headers'; +import { DynamicBox } from '@/components/scenarioBox'; export default async function Page() { await headers(); - return

Dynamic leaf: {Date.now()}

; + return ( + +

Dynamic leaf: {Date.now()}

+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx index 8e7de36d156a..494dee2483e2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout-cached-leaf/page.tsx @@ -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 ( -

- {id}:{Date.now()} -

+ +

+ {id}:{Date.now()} +

+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx index 412587fd8f33..4b9b4cffc9bf 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/dynamic-layouts/[id]/layout.tsx @@ -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
{children}
; + return ( +
+

Dynamic layout, cached leaf

+ +

request-time: {Date.now()}

+ {children} +
+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx index 2b0b77b6e3a9..7920ef335dc2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx @@ -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. @@ -14,11 +15,14 @@ export default async function ShortLivedLayout({ cacheLife({ revalidate: 1, expire: 2 }); const { id } = await params; return ( -
-

- {id}:{Date.now()} -

- {children} -
+
+

Mixed lifetimes

+ +

+ {id}:{Date.now()} +

+ {children} +
+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx index 18913cbe9790..c1c0e31494f6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx @@ -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 ( -

- {id}:{Date.now()} -

+ +

+ {id}:{Date.now()} +

+
); } export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; await headers(); - return ; + return ( + +

request-time: {Date.now()}

+ +
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx index ec8d5e21d947..606b9f01a2b2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/a/page.tsx @@ -1,6 +1,11 @@ import { headers } from 'next/headers'; +import { DynamicBox } from '@/components/scenarioBox'; export default async function Page() { await headers(); - return

Route a: {Date.now()}

; + return ( + +

Route a: {Date.now()}

+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx index 9a1b84b2ca28..18bff4a0ed35 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/b/page.tsx @@ -1,6 +1,11 @@ import { headers } from 'next/headers'; +import { DynamicBox } from '@/components/scenarioBox'; export default async function Page() { await headers(); - return

Route b: {Date.now()}

; + return ( + +

Route b: {Date.now()}

+
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx index 220fdcf92ca8..16fa37dbd71e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/(cached-nesting)/shared-layout/[id]/layout.tsx @@ -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({ @@ -14,11 +15,14 @@ export default async function SharedLayout({ const { id } = await params; await new Promise(resolve => setTimeout(resolve, 100)); return ( -
-

- {id}:{Date.now()} -

- {children} -
+
+

Shared layout across sibling routes

+ +

+ {id}:{Date.now()} +

+ {children} +
+
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx index c8b8858f7785..c3e4050539bb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cached-sibling-components/page.tsx @@ -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 ( -
- {label}:{id}:{Date.now()} -
+ +
+ {label}:{id}:{Date.now()} +
+
); } @@ -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 ( - <> + +

request-time: {Date.now()}

- +
); } export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) { return ( - Loading...}> - - +
+

Cached sibling components

+ Loading...}> + + +
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx index c8f9cee0b787..5ffd72655346 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx @@ -1,7 +1,12 @@ export default function Layout({ children }: { children: React.ReactNode }) { return ( - {children} + +
+ ← Cache scenarios +
+ {children} + ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx index 481d7e5e46e6..1115c4054b3f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/nested-caches/page.tsx @@ -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'; @@ -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
{JSON.stringify({ ...nested, renderedAt: Date.now() })}
; + return ( + + +
{JSON.stringify({ ...nested, renderedAt: Date.now() })}
+
+
+ ); } 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 ; + return ( + +

request-time: {Date.now()}

+ +
+ ); } export default function Page({ searchParams }: { searchParams: Promise<{ id?: string }> }) { return ( - Loading...}> - - +
+

Nested caches with different lifetimes

+ Loading...}> + + +
); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx index 433db8283beb..8f7d5cf7558e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx @@ -1,3 +1,115 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +type Scenario = { name: string; href: (id: string) => string; description: string }; + +const groups: { title: string; scenarios: Scenario[] }[] = [ + { + title: 'Route handlers (JSON)', + scenarios: [ + { + name: '/api/use-cache', + href: id => `/api/use-cache?id=${id}`, + description: 'cached function that lives for hours. The second request with the same id is a hit.', + }, + { + name: '/api/use-cache-expiring', + href: id => `/api/use-cache-expiring?id=${id}`, + description: 'the entry expires after 2 seconds. Request again after 3 seconds for a refill.', + }, + { + name: '/api/use-cache-swr', + href: id => `/api/use-cache-swr?id=${id}`, + description: + 'the entry goes stale after 2 seconds. A stale read serves the old value and refills in the background.', + }, + ], + }, + { + title: 'Rendered pages', + scenarios: [ + { + name: '/cached-sibling-components', + href: id => `/cached-sibling-components?id=${id}`, + description: 'two cached sibling components. Each section is its own cache entry.', + }, + { + name: '/nested-caches', + href: id => `/nested-caches?id=${id}`, + description: + 'a cached component that expires after 2 seconds and calls a cached function that lives for hours.', + }, + ], + }, + { + title: 'Nested layouts', + scenarios: [ + { + name: '/cached-mid-layout/[id]', + href: id => `/cached-mid-layout/${id}`, + description: 'a cached layout above a dynamic page.', + }, + { + name: '/dynamic-layouts/[id]/layout-cached-leaf', + href: id => `/dynamic-layouts/${id}/layout-cached-leaf`, + description: 'dynamic layouts above a cached leaf component.', + }, + { + name: '/mixed-lifetimes/[id]', + href: id => `/mixed-lifetimes/${id}`, + description: 'a cached layout that expires after 2 seconds around a cached component that lives for hours.', + }, + { + name: '/shared-layout/[id]/a', + href: id => `/shared-layout/${id}/a`, + description: 'route a fills the layout entry that routes a and b share.', + }, + { + name: '/shared-layout/[id]/b', + href: id => `/shared-layout/${id}/b`, + description: 'route b hits the layout entry that route a filled.', + }, + ], + }, + { + title: 'Pageload', + scenarios: [ + { + name: '/pageload-tracing', + href: () => '/pageload-tracing', + description: 'a prerendered shell with a dynamic hole. Check the pageload and server trace connection here.', + }, + ], + }, +]; + +// The links are plain `` elements so that every click is a full document request with its own +// server trace. `` would navigate through the client-side router instead. export default function Page() { - return

Next 16 CacheComponents test app

; + const [id, setId] = useState(''); + // Generated after hydration so the prerendered shell does not bake in one fixed id. + useEffect(() => setId(crypto.randomUUID()), []); + + return ( +
+

Next 16 Cache Components scenarios (span streaming)

+

+ Every link carries a random id that becomes part of the cache key. The first click fills the cache. A second + click on the same link, or a reload of the target page, hits it. Reload this page to get fresh ids. +

+ {groups.map(group => ( +
+

{group.title}

+
+
+ ))} +
+ ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx new file mode 100644 index 000000000000..cf2d318d64cc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/components/scenarioBox.tsx @@ -0,0 +1,34 @@ +import type { CSSProperties, ReactNode } from 'react'; + +const box: CSSProperties = { + padding: '8px 12px', + margin: '12px 0', + borderRadius: 8, +}; + +const badge: CSSProperties = { + display: 'inline-block', + fontSize: 12, + fontWeight: 600, + color: '#fff', + padding: '1px 8px', + borderRadius: 4, +}; + +export function CachedBox({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ Cached · {label} + {children} +
+ ); +} + +export function DynamicBox({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ Dynamic · {label} + {children} +
+ ); +}