diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 06c7c164a5d3..d8d33c448d42 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -38,7 +38,6 @@ import { createInitialRouterState, InitialRouterStateParameters, } from './router-reducer/create-initial-router-state' -import { fetchServerResponse } from './router-reducer/fetch-server-response' import { isBot } from '../../shared/lib/router/utils/is-bot' import { addBasePath } from '../add-base-path' import { AppRouterAnnouncer } from './app-router-announcer' @@ -64,8 +63,6 @@ const HotReloader: : (require('./react-dev-overlay/hot-reloader-client') .default as typeof import('./react-dev-overlay/hot-reloader-client').default) -const prefetched = new Set() - type AppRouterProps = Omit< Omit, 'initialParallelRoutes' @@ -184,41 +181,23 @@ function Router({ back: () => window.history.back(), forward: () => window.history.forward(), prefetch: async (href) => { - const hrefWithBasePath = addBasePath(href) - // If prefetch has already been triggered, don't trigger it again. - if ( - prefetched.has(hrefWithBasePath) || - (typeof window !== 'undefined' && isBot(window.navigator.userAgent)) - ) { + if (isBot(window.navigator.userAgent)) { return } - prefetched.add(hrefWithBasePath) - const url = new URL(hrefWithBasePath, location.origin) + const url = new URL(addBasePath(href), location.origin) // External urls can't be prefetched in the same way. if (isExternalURL(url)) { return } - try { - const routerTree = window.history.state?.tree || initialTree - const serverResponse = await fetchServerResponse( + + // @ts-ignore startTransition exists + React.startTransition(() => { + dispatch({ + type: ACTION_PREFETCH, url, - // initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case. - routerTree, - true - ) - // @ts-ignore startTransition exists - React.startTransition(() => { - dispatch({ - type: ACTION_PREFETCH, - url, - tree: routerTree, - serverResponse, - }) }) - } catch (err) { - console.error('PREFETCH ERROR', err) - } + }) }, replace: (href, options = {}) => { // @ts-ignore startTransition exists @@ -251,7 +230,7 @@ function Router({ } return routerInstance - }, [dispatch, initialTree]) + }, [dispatch]) useEffect(() => { // When mpaNavigation flag is set do a hard navigation to the new url. diff --git a/packages/next/src/client/components/router-reducer/apply-flight-data.ts b/packages/next/src/client/components/router-reducer/apply-flight-data.ts new file mode 100644 index 000000000000..7f01a860af65 --- /dev/null +++ b/packages/next/src/client/components/router-reducer/apply-flight-data.ts @@ -0,0 +1,33 @@ +import { CacheNode, CacheStates } from '../../../shared/lib/app-router-context' +import { FlightDataPath } from '../../../server/app-render/types' +import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head' +import { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data' +import { ReadonlyReducerState } from './router-reducer-types' + +export function applyFlightData( + state: ReadonlyReducerState, + cache: CacheNode, + flightDataPath: FlightDataPath +): boolean { + // The one before last item is the router state tree patch + const [treePatch, subTreeData, head] = flightDataPath.slice(-3) + + // Handles case where prefetch only returns the router tree patch without rendered components. + if (subTreeData === null) { + return false + } + + if (flightDataPath.length === 3) { + cache.status = CacheStates.READY + cache.subTreeData = subTreeData + fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head) + } else { + // Copy subTreeData for the root node of the cache. + cache.status = CacheStates.READY + cache.subTreeData = state.cache.subTreeData + // Create a copy of the existing cache with the subTreeData applied. + fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath) + } + + return true +} diff --git a/packages/next/src/client/components/router-reducer/create-href-from-url.ts b/packages/next/src/client/components/router-reducer/create-href-from-url.ts index 93fe7d3ec364..3ed33c67ceb5 100644 --- a/packages/next/src/client/components/router-reducer/create-href-from-url.ts +++ b/packages/next/src/client/components/router-reducer/create-href-from-url.ts @@ -1,5 +1,6 @@ export function createHrefFromUrl( - url: Pick + url: Pick, + includeHash: boolean = true ): string { - return url.pathname + url.search + url.hash + return url.pathname + url.search + (includeHash ? url.hash : '') } 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 63155ffa7edb..b49f888eb5df 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 @@ -40,8 +40,6 @@ describe('createInitialRouterState', () => { initialHead: Test, }) - console.log(initialParallelRoutes) - const state2 = createInitialRouterState({ initialTree, initialCanonicalUrl, diff --git a/packages/next/src/client/components/router-reducer/handle-mutable.ts b/packages/next/src/client/components/router-reducer/handle-mutable.ts new file mode 100644 index 000000000000..a3b6e3e9ddf3 --- /dev/null +++ b/packages/next/src/client/components/router-reducer/handle-mutable.ts @@ -0,0 +1,54 @@ +import { + Mutable, + ReadonlyReducerState, + ReducerState, +} from './router-reducer-types' + +export function handleMutable( + state: ReadonlyReducerState, + mutable: Mutable +): ReducerState { + return { + // Set href. + canonicalUrl: + typeof mutable.canonicalUrl !== 'undefined' + ? mutable.canonicalUrl === state.canonicalUrl + ? state.canonicalUrl + : mutable.canonicalUrl + : state.canonicalUrl, + pushRef: { + pendingPush: + typeof mutable.pendingPush !== 'undefined' + ? mutable.pendingPush + : state.pushRef.pendingPush, + mpaNavigation: + typeof mutable.mpaNavigation !== 'undefined' + ? mutable.mpaNavigation + : state.pushRef.mpaNavigation, + }, + // All navigation requires scroll and focus management to trigger. + focusAndScrollRef: { + apply: + typeof mutable.applyFocusAndScroll !== 'undefined' + ? mutable.applyFocusAndScroll + : state.focusAndScrollRef.apply, + hashFragment: + // Empty hash should trigger default behavior of scrolling layout into view. + // #top is handled in layout-router. + mutable.hashFragment && mutable.hashFragment !== '' + ? // Remove leading # and decode hash to make non-latin hashes work. + decodeURIComponent(mutable.hashFragment.slice(1)) + : null, + }, + // Apply cache. + cache: mutable.cache ? mutable.cache : state.cache, + prefetchCache: mutable.prefetchCache + ? mutable.prefetchCache + : state.prefetchCache, + // Apply patched router state. + tree: + typeof mutable.patchedTree !== 'undefined' + ? mutable.patchedTree + : state.tree, + } +} 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 69429f53811e..ed75c173f063 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 @@ -68,6 +68,7 @@ jest.mock('../fetch-server-response', () => { }, } }) + import { FlightRouterState } from '../../../../server/app-render/types' import { CacheNode, @@ -82,7 +83,7 @@ import { } from '../router-reducer-types' import { navigateReducer } from './navigate-reducer' import { prefetchReducer } from './prefetch-reducer' -import { fetchServerResponse } from '../fetch-server-response' +import { createRecordFromThenable } from '../create-record-from-thenable' const getInitialRouterStateTree = (): FlightRouterState => [ '', @@ -987,12 +988,9 @@ describe('navigateReducer', () => { ]) const url = new URL('/linking/about', 'https://localhost') - const serverResponse = await fetchServerResponse(url, initialTree, true) const prefetchAction: PrefetchAction = { type: ACTION_PREFETCH, url, - tree: initialTree, - serverResponse, } const state = createInitialRouterState({ @@ -1007,6 +1005,8 @@ describe('navigateReducer', () => { await runPromiseThrowChain(() => prefetchReducer(state, prefetchAction)) + await state.prefetchCache.get(url.pathname + url.search)?.data + const state2 = createInitialRouterState({ initialTree, initialHead: null, @@ -1018,6 +1018,7 @@ describe('navigateReducer', () => { }) await runPromiseThrowChain(() => prefetchReducer(state2, prefetchAction)) + await state2.prefetchCache.get(url.pathname + url.search)?.data const action: NavigateAction = { type: ACTION_NAVIGATE, @@ -1041,42 +1042,43 @@ describe('navigateReducer', () => { navigateReducer(state2, action) ) + const prom = Promise.resolve([ + [ + [ + 'children', + 'linking', + 'children', + 'about', + [ + 'about', + { + children: ['', {}], + }, + ], +

About Page!

, + + About page! + , + ], + ], + undefined, + ] as any) + const record = createRecordFromThenable(prom) + await prom + const expectedState: ReturnType = { prefetchCache: new Map([ [ '/linking/about', { - canonicalUrlOverride: undefined, - flightData: [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['', {}], - }, - ], -

About Page!

, - - About page! - , - ], - ], - tree: [ + data: record, + treeAtTimeOfPrefetch: [ '', { children: [ 'linking', { - children: [ - 'about', - { - children: ['', {}], - }, - ], + children: ['', {}], }, ], }, diff --git a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts index ba311fd143e2..24c8b590473d 100644 --- a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts @@ -1,17 +1,9 @@ -import { - CacheNode, - CacheStates, -} from '../../../../shared/lib/app-router-context' -import type { - FlightDataPath, - FlightSegmentPath, -} from '../../../../server/app-render/types' +import { CacheStates } from '../../../../shared/lib/app-router-context' +import type { FlightSegmentPath } from '../../../../server/app-render/types' import { fetchServerResponse } from '../fetch-server-response' import { createRecordFromThenable } from '../create-record-from-thenable' import { readRecordValue } from '../read-record-value' import { createHrefFromUrl } from '../create-href-from-url' -import { fillLazyItemsTillLeafWithHead } from '../fill-lazy-items-till-leaf-with-head' -import { fillCacheWithNewSubTreeData } from '../fill-cache-with-new-subtree-data' import { invalidateCacheBelowFlightSegmentPath } from '../invalidate-cache-below-flight-segmentpath' import { fillCacheWithDataProperty } from '../fill-cache-with-data-property' import { createOptimisticTree } from '../create-optimistic-tree' @@ -24,81 +16,8 @@ import type { ReadonlyReducerState, ReducerState, } from '../router-reducer-types' - -export function handleMutable( - state: ReadonlyReducerState, - mutable: Mutable -): ReducerState { - return { - // Set href. - canonicalUrl: - typeof mutable.canonicalUrl !== 'undefined' - ? mutable.canonicalUrl === state.canonicalUrl - ? state.canonicalUrl - : mutable.canonicalUrl - : state.canonicalUrl, - pushRef: { - pendingPush: - typeof mutable.pendingPush !== 'undefined' - ? mutable.pendingPush - : state.pushRef.pendingPush, - mpaNavigation: - typeof mutable.mpaNavigation !== 'undefined' - ? mutable.mpaNavigation - : state.pushRef.mpaNavigation, - }, - // All navigation requires scroll and focus management to trigger. - focusAndScrollRef: { - apply: - typeof mutable.applyFocusAndScroll !== 'undefined' - ? mutable.applyFocusAndScroll - : state.focusAndScrollRef.apply, - hashFragment: - // Empty hash should trigger default behavior of scrolling layout into view. - // #top is handled in layout-router. - mutable.hashFragment && mutable.hashFragment !== '' - ? // Remove leading # and decode hash to make non-latin hashes work. - decodeURIComponent(mutable.hashFragment.slice(1)) - : null, - }, - // Apply cache. - cache: mutable.cache ? mutable.cache : state.cache, - prefetchCache: state.prefetchCache, - // Apply patched router state. - tree: - typeof mutable.patchedTree !== 'undefined' - ? mutable.patchedTree - : state.tree, - } -} - -export function applyFlightData( - state: ReadonlyReducerState, - cache: CacheNode, - flightDataPath: FlightDataPath -): boolean { - // The one before last item is the router state tree patch - const [treePatch, subTreeData, head] = flightDataPath.slice(-3) - - // Handles case where prefetch only returns the router tree patch without rendered components. - if (subTreeData === null) { - return false - } - - if (flightDataPath.length === 3) { - cache.status = CacheStates.READY - cache.subTreeData = subTreeData - fillLazyItemsTillLeafWithHead(cache, state.cache, treePatch, head) - } else { - // Copy subTreeData for the root node of the cache. - cache.status = CacheStates.READY - cache.subTreeData = state.cache.subTreeData - // Create a copy of the existing cache with the subTreeData applied. - fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath) - } - - return true -} +import { handleMutable } from '../handle-mutable' +import { applyFlightData } from '../apply-flight-data' export function handleExternalUrl( state: ReadonlyReducerState, @@ -143,28 +62,52 @@ export function navigateReducer( return handleExternalUrl(state, mutable, url.toString(), pendingPush) } - const prefetchValues = state.prefetchCache.get(href) + const prefetchValues = state.prefetchCache.get(createHrefFromUrl(url, false)) if (prefetchValues) { // The one before last item is the router state tree patch - const { flightData, tree: newTree, canonicalUrlOverride } = prefetchValues + const { treeAtTimeOfPrefetch, data } = prefetchValues + + // Unwrap cache data with `use` to suspend here (in the reducer) until the fetch resolves. + const [flightData, canonicalUrlOverride] = readRecordValue(data!) // Handle case when navigating to page in `pages` from `app` if (typeof flightData === 'string') { return handleExternalUrl(state, mutable, flightData, pendingPush) } + // TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths. + const flightDataPath = flightData[0] + const flightSegmentPath = flightDataPath.slice( + 0, + -3 + ) as unknown as FlightSegmentPath + // The one before last item is the router state tree patch + const [treePatch] = flightDataPath.slice(-3) + + // Create new tree based on the flightSegmentPath and router state patch + let newTree = applyRouterStatePatchToTree( + // TODO-APP: remove '' + ['', ...flightSegmentPath], + state.tree, + treePatch + ) + + // If the tree patch can't be applied to the current tree then we use the tree at time of prefetch + // TODO-APP: This should instead fill in the missing pieces in `state.tree` with the data from `treeAtTimeOfPrefetch`, then apply the patch. + if (newTree === null) { + newTree = applyRouterStatePatchToTree( + // TODO-APP: remove '' + ['', ...flightSegmentPath], + treeAtTimeOfPrefetch, + treePatch + ) + } + if (newTree !== null) { if (isNavigatingToNewRootLayout(state.tree, newTree)) { return handleExternalUrl(state, mutable, href, pendingPush) } - // TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths. - const flightDataPath = flightData[0] - const flightSegmentPath = flightDataPath.slice( - 0, - -3 - ) as unknown as FlightSegmentPath - const applied = applyFlightData(state, cache, flightDataPath) const hardNavigate = diff --git a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx index 7ee4d7f88c24..e645e4533f92 100644 --- a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx +++ b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx @@ -41,6 +41,7 @@ import { createInitialRouterState } from '../create-initial-router-state' import { PrefetchAction, ACTION_PREFETCH } from '../router-reducer-types' import { prefetchReducer } from './prefetch-reducer' import { fetchServerResponse } from '../fetch-server-response' +import { createRecordFromThenable } from '../create-record-from-thenable' const getInitialRouterStateTree = (): FlightRouterState => [ '', @@ -127,33 +128,29 @@ describe('prefetchReducer', () => { const action: PrefetchAction = { type: ACTION_PREFETCH, url, - tree: initialTree, - serverResponse, } const newState = await runPromiseThrowChain(() => prefetchReducer(state, action) ) + const prom = Promise.resolve(serverResponse) + const record = createRecordFromThenable(prom) + await prom + const expectedState: ReturnType = { prefetchCache: new Map([ [ '/linking/about', { - canonicalUrlOverride: undefined, - flightData: serverResponse[0], - tree: [ + data: record, + treeAtTimeOfPrefetch: [ '', { children: [ 'linking', { - children: [ - 'about', - { - children: ['', {}], - }, - ], + children: ['', {}], }, ], }, @@ -269,8 +266,6 @@ describe('prefetchReducer', () => { const action: PrefetchAction = { type: ACTION_PREFETCH, url, - tree: initialTree, - serverResponse, } await runPromiseThrowChain(() => prefetchReducer(state, action)) @@ -279,25 +274,23 @@ describe('prefetchReducer', () => { prefetchReducer(state2, action) ) + const prom = Promise.resolve(serverResponse) + const record = createRecordFromThenable(prom) + await prom + const expectedState: ReturnType = { prefetchCache: new Map([ [ '/linking/about', { - canonicalUrlOverride: undefined, - flightData: serverResponse[0], - tree: [ + data: record, + treeAtTimeOfPrefetch: [ '', { children: [ 'linking', { - children: [ - 'about', - { - children: ['', {}], - }, - ], + children: ['', {}], }, ], }, diff --git a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts index 92b8e7db00c4..7749b9009052 100644 --- a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts @@ -1,50 +1,43 @@ -import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree' import { createHrefFromUrl } from '../create-href-from-url' +import { fetchServerResponse } from '../fetch-server-response' import { PrefetchAction, ReducerState, ReadonlyReducerState, } from '../router-reducer-types' +import { createRecordFromThenable } from '../create-record-from-thenable' export function prefetchReducer( state: ReadonlyReducerState, action: PrefetchAction ): ReducerState { - const { url, serverResponse } = action - const [flightData, canonicalUrlOverride] = serverResponse + const { url } = action + const href = createHrefFromUrl( + url, + // Ensures the hash is not part of the cache key as it does not affect fetching the server + false + ) - if (typeof flightData === 'string') { + // If the href was already prefetched it is not necessary to prefetch it again + if (state.prefetchCache.has(href)) { return state } - const href = createHrefFromUrl(url) - - // TODO-APP: Currently the Flight data can only have one item but in the future it can have multiple paths. - const flightDataPath = flightData[0] - - // The one before last item is the router state tree patch - const [treePatch] = flightDataPath.slice(-3) - - const flightSegmentPath = flightDataPath.slice(0, -3) - - const newTree = applyRouterStatePatchToTree( - // TODO-APP: remove '' - ['', ...flightSegmentPath], - state.tree, - treePatch + // fetchServerResponse is intentionally not awaited so that it can be unwrapped in the navigate-reducer + const serverResponse = createRecordFromThenable( + fetchServerResponse( + url, + // initialTree is used when history.state.tree is missing because the history state is set in `useEffect` below, it being missing means this is the hydration case. + state.tree, + true + ) ) - // Patch did not apply correctly - if (newTree === null) { - return state - } - // Create new tree based on the flightSegmentPath and router state patch state.prefetchCache.set(href, { - flightData, // Create new tree based on the flightSegmentPath and router state patch - tree: newTree, - canonicalUrlOverride, + treeAtTimeOfPrefetch: state.tree, + data: serverResponse, }) return state diff --git a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx index 5c261aada2e2..2ca7f4556a04 100644 --- a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx +++ b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx @@ -378,7 +378,7 @@ describe('refreshReducer', () => { expect(newState).toMatchObject(expectedState) }) - it('should preserve existing segments (concurrent)', async () => { + it('should invalidate all segments (concurrent)', async () => { const initialTree = getInitialRouterStateTree() const initialCanonicalUrl = '/linking' const children = ( @@ -536,10 +536,211 @@ describe('refreshReducer', () => { subTreeData: null, }, ], - [ + ]), + ], + ]), + }, + tree: [ + '', + { + children: [ + 'linking', + { + children: ['', {}], + }, + ], + }, + undefined, + undefined, + true, + ], + } + + expect(newState).toMatchObject(expectedState) + }) + + it('should invalidate prefetchCache (concurrent)', async () => { + const initialTree = getInitialRouterStateTree() + const initialCanonicalUrl = '/linking' + const children = ( + + + Root layout + + ) + const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map([ + [ + 'children', + new Map([ + [ + 'linking', + { + status: CacheStates.READY, + parallelRoutes: new Map([ + [ + 'children', + new Map([ + [ + '', + { + status: CacheStates.READY, + data: null, + subTreeData: <>Linking page, + parallelRoutes: new Map(), + }, + ], + ]), + ], + ]), + data: null, + subTreeData: <>Linking layout level, + }, + ], + [ + 'about', + { + status: CacheStates.READY, + parallelRoutes: new Map([ + [ + 'children', + new Map([ + [ + '', + { + status: CacheStates.READY, + data: null, + subTreeData: <>About page, + parallelRoutes: new Map(), + }, + ], + ]), + ], + ]), + data: null, + subTreeData: <>About layout level, + }, + ], + ]), + ], + ]) + + const prefetchItem = { + canonicalUrlOverride: undefined, + flightData: [ + [ + '', + { + children: [ + 'linking', + { + children: [ + 'about', + { + children: ['', {}], + }, + ], + }, + ], + }, + undefined, + undefined, + true, + ], + <>About, + <>Head, + ], + tree: [ + '', + { + children: [ + 'linking', + { + children: [ 'about', { - status: CacheStates.READY, + children: ['', {}], + }, + ], + }, + ], + }, + undefined, + undefined, + true, + ], + } + + const state = createInitialRouterState({ + initialTree, + initialHead: null, + initialCanonicalUrl, + children, + initialParallelRoutes, + isServer: false, + location: new URL('/linking', 'https://localhost') as any, + }) + + state.prefetchCache.set('/linking/about', prefetchItem) + + const state2 = createInitialRouterState({ + initialTree, + initialHead: null, + initialCanonicalUrl, + children, + initialParallelRoutes, + isServer: false, + location: new URL('/linking', 'https://localhost') as any, + }) + state2.prefetchCache.set('/linking/about', prefetchItem) + + const action: RefreshAction = { + type: ACTION_REFRESH, + cache: { + status: CacheStates.LAZY_INITIALIZED, + data: null, + subTreeData: null, + parallelRoutes: new Map(), + }, + mutable: {}, + origin: new URL('/linking', 'https://localhost').origin, + } + + await runPromiseThrowChain(() => refreshReducer(state, action)) + + const newState = await runPromiseThrowChain(() => + refreshReducer(state2, action) + ) + + const expectedState: ReturnType = { + prefetchCache: new Map(), + pushRef: { + mpaNavigation: false, + pendingPush: false, + }, + focusAndScrollRef: { + apply: false, + hashFragment: null, + }, + canonicalUrl: '/linking', + cache: { + status: CacheStates.READY, + data: null, + subTreeData: ( + + + +

Linking Page!

+ + + ), + parallelRoutes: new Map([ + [ + 'children', + new Map([ + [ + 'linking', + { + status: CacheStates.LAZY_INITIALIZED, parallelRoutes: new Map([ [ 'children', @@ -547,17 +748,22 @@ describe('refreshReducer', () => { [ '', { - status: CacheStates.READY, + status: CacheStates.LAZY_INITIALIZED, data: null, - subTreeData: <>About page, + subTreeData: null, parallelRoutes: new Map(), + head: ( + <> + Linking page! + + ), }, ], ]), ], ]), data: null, - subTreeData: <>About layout level, + subTreeData: null, }, ], ]), diff --git a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts index bdde126d142b..11087da3221f 100644 --- a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts @@ -9,11 +9,10 @@ import { ReducerState, RefreshAction, } from '../router-reducer-types' -import { - handleMutable, - applyFlightData, - handleExternalUrl, -} from './navigate-reducer' +import { handleExternalUrl } from './navigate-reducer' +import { handleMutable } from '../handle-mutable' +import { CacheStates } from '../../../../shared/lib/app-router-context' +import { fillLazyItemsTillLeafWithHead } from '../fill-lazy-items-till-leaf-with-head' export function refreshReducer( state: ReadonlyReducerState, @@ -91,10 +90,22 @@ export function refreshReducer( mutable.canonicalUrl = canonicalUrlOverrideHref } - const applied = applyFlightData(state, cache, flightDataPath) - - if (applied) { + // The one before last item is the router state tree patch + const [subTreeData, head] = flightDataPath.slice(-2) + + // Handles case where prefetch only returns the router tree patch without rendered components. + if (subTreeData !== null) { + cache.status = CacheStates.READY + cache.subTreeData = subTreeData + fillLazyItemsTillLeafWithHead( + cache, + // Existing cache is not passed in as `router.refresh()` has to invalidate the entire cache. + undefined, + treePatch, + head + ) mutable.cache = cache + mutable.prefetchCache = new Map() } mutable.previousTree = state.tree diff --git a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts index a8956ed65ddf..0946e92709e4 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts @@ -6,11 +6,9 @@ import { ReducerState, ReadonlyReducerState, } from '../router-reducer-types' -import { - handleMutable, - applyFlightData, - handleExternalUrl, -} from './navigate-reducer' +import { handleExternalUrl } from './navigate-reducer' +import { applyFlightData } from '../apply-flight-data' +import { handleMutable } from '../handle-mutable' export function serverPatchReducer( state: ReadonlyReducerState, diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index e589b154307a..3a8fa5db8f52 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -19,6 +19,7 @@ export interface Mutable { applyFocusAndScroll?: boolean pendingPush?: boolean cache?: CacheNode + prefetchCache?: AppRouterState['prefetchCache'] hashFragment?: string } @@ -116,8 +117,6 @@ export interface ServerPatchAction { export interface PrefetchAction { type: typeof ACTION_PREFETCH url: URL - tree: FlightRouterState - serverResponse: Awaited> } interface PushRef { @@ -164,9 +163,8 @@ export type AppRouterState = { prefetchCache: Map< string, { - flightData: FlightData - tree: FlightRouterState - canonicalUrlOverride: URL | undefined + treeAtTimeOfPrefetch: FlightRouterState + data: ReturnType | null } > /**