diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx
index e096a38f1836b..483dc8d0391fb 100644
--- a/packages/next/src/client/components/app-router.tsx
+++ b/packages/next/src/client/components/app-router.tsx
@@ -264,6 +264,7 @@ function Router({
initialTree,
initialCanonicalUrl,
initialSeedData,
+ initialFlightData,
assetPrefix,
missingSlots,
}: AppRouterProps) {
@@ -275,11 +276,18 @@ function Router({
initialCanonicalUrl,
initialTree,
initialParallelRoutes,
- isServer,
location: !isServer ? window.location : null,
initialHead,
+ initialFlightData,
}),
- [buildId, initialSeedData, initialCanonicalUrl, initialTree, initialHead]
+ [
+ buildId,
+ initialSeedData,
+ initialCanonicalUrl,
+ initialTree,
+ initialHead,
+ initialFlightData,
+ ]
)
const [reducerState, dispatch, sync] =
useReducerWithReduxDevtools(initialState)
diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx b/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx
index 523a259fbac9a..f959febfa6940 100644
--- a/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx
+++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import type { FlightRouterState } from '../../../server/app-render/types'
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
import { createInitialRouterState } from './create-initial-router-state'
+import { PrefetchKind } from './router-reducer-types'
const buildId = 'development'
@@ -37,8 +38,8 @@ describe('createInitialRouterState', () => {
initialTree,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking', 'https://localhost') as any,
initialHead:
Test,
})
@@ -48,8 +49,8 @@ describe('createInitialRouterState', () => {
initialTree,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking', 'https://localhost') as any,
initialHead: Test,
})
@@ -96,7 +97,19 @@ describe('createInitialRouterState', () => {
buildId,
tree: initialTree,
canonicalUrl: initialCanonicalUrl,
- prefetchCache: new Map(),
+ prefetchCache: new Map([
+ [
+ '/linking',
+ {
+ key: '/linking',
+ data: expect.any(Promise),
+ prefetchTime: expect.any(Number),
+ kind: PrefetchKind.AUTO,
+ lastUsedTime: null,
+ treeAtTimeOfPrefetch: initialTree,
+ },
+ ],
+ ]),
pushRef: {
pendingPush: false,
mpaNavigation: false,
diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
index deaca80dc00ff..1b98ae0f6d1e0 100644
--- a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
+++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
@@ -3,11 +3,14 @@ import type { CacheNode } from '../../../shared/lib/app-router-context.shared-ru
import type {
FlightRouterState,
CacheNodeSeedData,
+ FlightData,
} from '../../../server/app-render/types'
import { createHrefFromUrl } from './create-href-from-url'
import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head'
import { extractPathFromFlightRouterState } from './compute-changed-path'
+import { createPrefetchCacheKey } from './reducers/prefetch-cache-utils'
+import { PrefetchKind, type PrefetchCacheEntry } from './router-reducer-types'
export interface InitialRouterStateParameters {
buildId: string
@@ -15,7 +18,7 @@ export interface InitialRouterStateParameters {
initialCanonicalUrl: string
initialSeedData: CacheNodeSeedData
initialParallelRoutes: CacheNode['parallelRoutes']
- isServer: boolean
+ initialFlightData: FlightData
location: Location | null
initialHead: ReactNode
}
@@ -26,10 +29,11 @@ export function createInitialRouterState({
initialSeedData,
initialCanonicalUrl,
initialParallelRoutes,
- isServer,
+ initialFlightData,
location,
initialHead,
}: InitialRouterStateParameters) {
+ const isServer = !location
const rsc = initialSeedData[2]
const cache: CacheNode = {
@@ -40,6 +44,25 @@ export function createInitialRouterState({
parallelRoutes: isServer ? new Map() : initialParallelRoutes,
}
+ const prefetchCache = new Map()
+
+ if (location && initialFlightData.length > 0) {
+ // Seed the prefetch cache with this page's data.
+ // This is to prevent needlessly re-prefetching a page that is already reusable,
+ // and will avoid triggering a loading state/data fetch stall when navigating back to the page.
+ const url = new URL(location.pathname, location.origin)
+ const cacheKey = createPrefetchCacheKey(url)
+
+ prefetchCache.set(cacheKey, {
+ data: Promise.resolve([initialFlightData, undefined, false, false]),
+ kind: PrefetchKind.AUTO,
+ lastUsedTime: null,
+ prefetchTime: Date.now(),
+ key: cacheKey,
+ treeAtTimeOfPrefetch: initialTree,
+ })
+ }
+
// When the cache hasn't been seeded yet we fill the cache with the head.
if (initialParallelRoutes === null || initialParallelRoutes.size === 0) {
fillLazyItemsTillLeafWithHead(
@@ -55,7 +78,7 @@ export function createInitialRouterState({
buildId,
tree: initialTree,
cache,
- prefetchCache: new Map(),
+ prefetchCache,
pushRef: {
pendingPush: false,
mpaNavigation: false,
diff --git a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx
index b7cf15ce625f4..387efa7a7c072 100644
--- a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx
+++ b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx
@@ -163,8 +163,8 @@ describe('navigateReducer', () => {
initialHead: null,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking', 'https://localhost') as any,
})
const action: NavigateAction = {
@@ -254,6 +254,30 @@ describe('navigateReducer', () => {
},
"nextUrl": "/linking/about",
"prefetchCache": Map {
+ "/linking" => {
+ "data": Promise {},
+ "key": "/linking",
+ "kind": "auto",
+ "lastUsedTime": null,
+ "prefetchTime": 1690329600000,
+ "treeAtTimeOfPrefetch": [
+ "",
+ {
+ "children": [
+ "linking",
+ {
+ "children": [
+ "__PAGE__",
+ {},
+ ],
+ },
+ ],
+ },
+ undefined,
+ undefined,
+ true,
+ ],
+ },
"/linking/about" => {
"data": Promise {},
"key": "/linking/about",
@@ -357,8 +381,8 @@ describe('navigateReducer', () => {
initialHead: null,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking', 'https://localhost') as any,
})
@@ -449,6 +473,30 @@ describe('navigateReducer', () => {
},
"nextUrl": "/linking/about",
"prefetchCache": Map {
+ "/linking" => {
+ "data": Promise {},
+ "key": "/linking",
+ "kind": "auto",
+ "lastUsedTime": null,
+ "prefetchTime": 1690329600000,
+ "treeAtTimeOfPrefetch": [
+ "",
+ {
+ "children": [
+ "linking",
+ {
+ "children": [
+ "__PAGE__",
+ {},
+ ],
+ },
+ ],
+ },
+ undefined,
+ undefined,
+ true,
+ ],
+ },
"/linking/about" => {
"data": Promise {},
"key": "/linking/about",
@@ -552,8 +600,8 @@ describe('navigateReducer', () => {
initialHead: null,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking', 'https://localhost') as any,
})
@@ -615,7 +663,32 @@ describe('navigateReducer', () => {
"segmentPaths": [],
},
"nextUrl": "/linking",
- "prefetchCache": Map {},
+ "prefetchCache": Map {
+ "/linking" => {
+ "data": Promise {},
+ "key": "/linking",
+ "kind": "auto",
+ "lastUsedTime": null,
+ "prefetchTime": 1690329600000,
+ "treeAtTimeOfPrefetch": [
+ "",
+ {
+ "children": [
+ "linking",
+ {
+ "children": [
+ "__PAGE__",
+ {},
+ ],
+ },
+ ],
+ },
+ undefined,
+ undefined,
+ true,
+ ],
+ },
+ },
"pushRef": {
"mpaNavigation": true,
"pendingPush": true,
@@ -689,8 +762,8 @@ describe('navigateReducer', () => {
initialHead: null,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking', 'https://localhost') as any,
})
@@ -752,7 +825,32 @@ describe('navigateReducer', () => {
"segmentPaths": [],
},
"nextUrl": "/linking",
- "prefetchCache": Map {},
+ "prefetchCache": Map {
+ "/linking" => {
+ "data": Promise {},
+ "key": "/linking",
+ "kind": "auto",
+ "lastUsedTime": null,
+ "prefetchTime": 1690329600000,
+ "treeAtTimeOfPrefetch": [
+ "",
+ {
+ "children": [
+ "linking",
+ {
+ "children": [
+ "__PAGE__",
+ {},
+ ],
+ },
+ ],
+ },
+ undefined,
+ undefined,
+ true,
+ ],
+ },
+ },
"pushRef": {
"mpaNavigation": true,
"pendingPush": false,
@@ -826,8 +924,8 @@ describe('navigateReducer', () => {
initialHead: null,
initialCanonicalUrl,
initialSeedData: ['', {}, children],
+ initialFlightData: [['']],
initialParallelRoutes,
- isServer: false,
location: new URL('/linking#hash', 'https://localhost') as any,
})
@@ -878,7 +976,7 @@ describe('navigateReducer', () => {