From 5947b03ce2644e593461b99a7771e21fa08a1907 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 3 Mar 2023 10:13:10 +0100 Subject: [PATCH 1/7] Ensure router.refresh() matches revalidatePath('/') behavior --- .../router-reducer/apply-flight-data.ts | 33 ++++++++ .../router-reducer/handle-mutable.ts | 47 +++++++++++ .../reducers/navigate-reducer.ts | 82 +------------------ .../reducers/refresh-reducer.test.tsx | 24 ------ .../reducers/refresh-reducer.ts | 26 ++++-- .../reducers/server-patch-reducer.ts | 8 +- .../router-reducer/router-reducer-types.ts | 1 + 7 files changed, 106 insertions(+), 115 deletions(-) create mode 100644 packages/next/src/client/components/router-reducer/apply-flight-data.ts create mode 100644 packages/next/src/client/components/router-reducer/handle-mutable.ts 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..8f6746dcd10b --- /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' +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/handle-mutable.ts b/packages/next/src/client/components/router-reducer/handle-mutable.ts new file mode 100644 index 000000000000..85bf7b724342 --- /dev/null +++ b/packages/next/src/client/components/router-reducer/handle-mutable.ts @@ -0,0 +1,47 @@ +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, + }, + // 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.ts b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts index 76fe666c6f13..80c69059573d 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' +import { CacheStates } from '../../../../shared/lib/app-router-context' +import type { FlightSegmentPath } from '../../../../server/app-render' 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,74 +16,8 @@ import { 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, - }, - // 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, 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 1444f35c34ac..dbe9ca57ce40 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 @@ -533,30 +533,6 @@ describe('refreshReducer', () => { subTreeData: null, }, ], - [ - '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, - }, - ], ]), ], ]), 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..95dfd5d7e978 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,9 +90,20 @@ 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 } 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 27d5b69f4ded..2ec3ba625236 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 @@ -16,6 +16,7 @@ export interface Mutable { applyFocusAndScroll?: boolean pendingPush?: boolean cache?: CacheNode + prefetchCache?: AppRouterState['prefetchCache'] } /** From 45c884a84ee1b35c43d3dc523be053ab5b2c4a3f Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 3 Mar 2023 10:21:02 +0100 Subject: [PATCH 2/7] Invalidate prefetch cache on router.refresh() --- .../reducers/refresh-reducer.test.tsx | 231 +++++++++++++++++- .../reducers/refresh-reducer.ts | 1 + 2 files changed, 231 insertions(+), 1 deletion(-) 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 dbe9ca57ce40..9b9871356677 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 @@ -376,7 +376,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 = ( @@ -555,4 +555,233 @@ describe('refreshReducer', () => { 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', + { + 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, + }, + 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', + new Map([ + [ + '', + { + status: CacheStates.LAZY_INITIALIZED, + data: null, + subTreeData: null, + parallelRoutes: new Map(), + head: ( + <> + Linking page! + + ), + }, + ], + ]), + ], + ]), + data: null, + subTreeData: null, + }, + ], + ]), + ], + ]), + }, + tree: [ + '', + { + children: [ + 'linking', + { + children: ['', {}], + }, + ], + }, + undefined, + undefined, + true, + ], + } + + expect(newState).toMatchObject(expectedState) + }) }) 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 95dfd5d7e978..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 @@ -105,6 +105,7 @@ export function refreshReducer( head ) mutable.cache = cache + mutable.prefetchCache = new Map() } mutable.previousTree = state.tree From 8669f4fa80a826d0a67bb59ad61b35d15fa8ca95 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 14 Mar 2023 15:25:42 +0100 Subject: [PATCH 3/7] Remove `prefetched` variable from app-router --- .../next/src/client/components/app-router.tsx | 36 ++++---------- .../router-reducer/create-href-from-url.ts | 5 +- .../reducers/navigate-reducer.ts | 42 +++++++++++++---- .../reducers/prefetch-reducer.ts | 47 ++++++++----------- .../router-reducer/router-reducer-types.ts | 7 +-- 5 files changed, 66 insertions(+), 71 deletions(-) diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 49e263cac463..c3024c44bbe5 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -60,8 +60,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' @@ -180,41 +178,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 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/reducers/navigate-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts index 80c69059573d..df171ce69a37 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 @@ -62,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.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/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 2ec3ba625236..c6ac48ba3736 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 @@ -113,8 +113,6 @@ export interface ServerPatchAction { export interface PrefetchAction { type: typeof ACTION_PREFETCH url: URL - tree: FlightRouterState - serverResponse: Awaited> } interface PushRef { @@ -157,9 +155,8 @@ export type AppRouterState = { prefetchCache: Map< string, { - flightData: FlightData - tree: FlightRouterState - canonicalUrlOverride: URL | undefined + treeAtTimeOfPrefetch: FlightRouterState + data: ReturnType | null } > /** From b8a95db9ea392839942b39367e4e7596914dac20 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 14 Mar 2023 16:15:46 +0100 Subject: [PATCH 4/7] Apply changes from merge --- .../components/router-reducer/handle-mutable.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/next/src/client/components/router-reducer/handle-mutable.ts b/packages/next/src/client/components/router-reducer/handle-mutable.ts index 85bf7b724342..d62b693ec217 100644 --- a/packages/next/src/client/components/router-reducer/handle-mutable.ts +++ b/packages/next/src/client/components/router-reducer/handle-mutable.ts @@ -32,12 +32,17 @@ export function handleMutable( 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, + prefetchCache: state.prefetchCache, // Apply patched router state. tree: typeof mutable.patchedTree !== 'undefined' From 85cec2c302afb578c14fb5fe62b9e8fbf2c9a156 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 15 Mar 2023 11:39:59 +0100 Subject: [PATCH 5/7] Update tests --- .../create-initial-router-state.test.tsx | 2 - .../router-reducer/handle-mutable.ts | 4 +- .../reducers/navigate-reducer.test.tsx | 63 ++++++++++--------- .../reducers/prefetch-reducer.test.tsx | 37 +++++------ .../reducers/refresh-reducer.test.tsx | 1 + 5 files changed, 52 insertions(+), 55 deletions(-) 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 0b6135db2428..779b54a000e5 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 index d62b693ec217..a3b6e3e9ddf3 100644 --- a/packages/next/src/client/components/router-reducer/handle-mutable.ts +++ b/packages/next/src/client/components/router-reducer/handle-mutable.ts @@ -42,7 +42,9 @@ export function handleMutable( }, // Apply cache. cache: mutable.cache ? mutable.cache : state.cache, - prefetchCache: state.prefetchCache, + prefetchCache: mutable.prefetchCache + ? mutable.prefetchCache + : state.prefetchCache, // Apply patched router state. tree: typeof mutable.patchedTree !== 'undefined' 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 12abb3cc71a3..f20d84d54b50 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,8 @@ jest.mock('../fetch-server-response', () => { }, } }) + +import { fetchServerResponse } from '../fetch-server-response' import { FlightRouterState } from '../../../../server/app-render' import { CacheNode, @@ -82,7 +84,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 +989,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 +1006,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 +1019,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 +1043,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/prefetch-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.test.tsx index d7e3a128791c..8d86aa2b7239 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/refresh-reducer.test.tsx b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.test.tsx index 3cd75cfa9441..a032a47e1e45 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 @@ -719,6 +719,7 @@ describe('refreshReducer', () => { }, focusAndScrollRef: { apply: false, + hashFragment: null, }, canonicalUrl: '/linking', cache: { From 8662a38023c9f7703f0c4e9746b3a8624832491a Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 15 Mar 2023 15:01:50 +0100 Subject: [PATCH 6/7] Fix import --- .../src/client/components/router-reducer/apply-flight-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 8f6746dcd10b..7f01a860af65 100644 --- a/packages/next/src/client/components/router-reducer/apply-flight-data.ts +++ b/packages/next/src/client/components/router-reducer/apply-flight-data.ts @@ -1,5 +1,5 @@ import { CacheNode, CacheStates } from '../../../shared/lib/app-router-context' -import { FlightDataPath } from '../../../server/app-render' +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' From 3cf970ff5a63df6136b4809dfc594f76fd60d4e2 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 15 Mar 2023 15:23:10 +0100 Subject: [PATCH 7/7] Fix lint --- packages/next/src/client/components/app-router.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 2ed2184624a0..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' @@ -231,7 +230,7 @@ function Router({ } return routerInstance - }, [dispatch, initialTree]) + }, [dispatch]) useEffect(() => { // When mpaNavigation flag is set do a hard navigation to the new url.