Add param names into the tree (#38415)
- Remove cache value that was incorrectly nested deeper - Remove extra useEffect (already applied during hydration based on the `useReducer` input) - Add dynamic parameter name into the tree Follow-up to #37551, cleans up some code and prepares for catch-all and optional catch-all routes.
This commit is contained in:
parent
3411794607
commit
0299f14a7e
5 changed files with 93 additions and 54 deletions
|
@ -161,13 +161,6 @@ export default function AppRouter({
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'UPDATE URL',
|
||||
pushRef.pendingPush ? 'push' : 'replace',
|
||||
pushRef.mpaNavigation ? 'MPA' : '',
|
||||
tree
|
||||
)
|
||||
|
||||
if (pushRef.mpaNavigation) {
|
||||
window.location.href = canonicalUrl
|
||||
return
|
||||
|
@ -213,10 +206,6 @@ export default function AppRouter({
|
|||
}
|
||||
}, [onPopState])
|
||||
|
||||
React.useEffect(() => {
|
||||
window.history.replaceState({ tree: initialTree }, '')
|
||||
}, [initialTree])
|
||||
|
||||
return (
|
||||
<PathnameContext.Provider value={pathname}>
|
||||
<QueryContext.Provider value={query}>
|
||||
|
|
|
@ -11,11 +11,12 @@ import {
|
|||
FullAppTreeContext,
|
||||
} from '../../shared/lib/app-router-context'
|
||||
import { fetchServerResponse } from './app-router.client'
|
||||
import { matchSegment } from './match-segments'
|
||||
|
||||
let infinitePromise: Promise<void> | Error
|
||||
|
||||
function equalArray(a: any[], b: any[]) {
|
||||
return a.length === b.length && a.every((val, i) => val === b[i])
|
||||
return a.length === b.length && a.every((val, i) => matchSegment(val, b[i]))
|
||||
}
|
||||
|
||||
function pathMatches(
|
||||
|
@ -29,11 +30,12 @@ function pathMatches(
|
|||
|
||||
function createInfinitePromise() {
|
||||
if (!infinitePromise) {
|
||||
infinitePromise = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
infinitePromise = new Error('Infinite promise')
|
||||
resolve()
|
||||
}, 20000)
|
||||
infinitePromise = new Promise((/* resolve */) => {
|
||||
// Note: this is used to debug when the rendering is never updated.
|
||||
// setTimeout(() => {
|
||||
// infinitePromise = new Error('Infinite promise')
|
||||
// resolve()
|
||||
// }, 5000)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ export function InnerLayoutRouter({
|
|||
childNodes.set(path, {
|
||||
data: null,
|
||||
subTreeData: childProp.current,
|
||||
parallelRoutes: new Map([[parallelRouterKey, new Map()]]),
|
||||
parallelRoutes: new Map(),
|
||||
})
|
||||
childProp.current = null
|
||||
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
|
||||
|
@ -128,7 +130,7 @@ export function InnerLayoutRouter({
|
|||
childNodes.set(path, {
|
||||
data,
|
||||
subTreeData: null,
|
||||
parallelRoutes: new Map([[parallelRouterKey, new Map()]]),
|
||||
parallelRoutes: new Map(),
|
||||
})
|
||||
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
|
||||
childNode = childNodes.get(path)
|
||||
|
@ -243,7 +245,13 @@ export default function OuterLayoutRouter({
|
|||
|
||||
// This relates to the segments in the current router
|
||||
// tree[1].children[0] refers to tree.children.segment in the data format
|
||||
const currentChildSegment = tree[1][parallelRouterKey][0] ?? childProp.segment
|
||||
const treeSegment = tree[1][parallelRouterKey][0]
|
||||
const childPropSegment = Array.isArray(childProp.segment)
|
||||
? childProp.segment[1]
|
||||
: childProp.segment
|
||||
const currentChildSegment =
|
||||
(Array.isArray(treeSegment) ? treeSegment[1] : treeSegment) ??
|
||||
childPropSegment
|
||||
const preservedSegments: string[] = [currentChildSegment]
|
||||
|
||||
return (
|
||||
|
@ -257,7 +265,7 @@ export default function OuterLayoutRouter({
|
|||
tree={tree}
|
||||
childNodes={childNodesForParallelRouter!}
|
||||
childProp={
|
||||
childProp.segment === preservedSegment ? childProp : null
|
||||
childPropSegment === preservedSegment ? childProp : null
|
||||
}
|
||||
segmentPath={segmentPath}
|
||||
path={preservedSegment}
|
||||
|
|
20
packages/next/client/components/match-segments.ts
Normal file
20
packages/next/client/components/match-segments.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Segment } from '../../server/app-render'
|
||||
|
||||
export const matchSegment = (
|
||||
existingSegment: Segment,
|
||||
segment: Segment
|
||||
): boolean => {
|
||||
// Common case: segment is just a string
|
||||
if (typeof existingSegment === 'string' && typeof segment === 'string') {
|
||||
return existingSegment === segment
|
||||
}
|
||||
|
||||
// Dynamic parameter case: segment is an array with param/value. Both param and value are compared.
|
||||
if (Array.isArray(existingSegment) && Array.isArray(segment)) {
|
||||
return (
|
||||
existingSegment[0] === segment[0] && existingSegment[1] === segment[1]
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -4,6 +4,7 @@ import type {
|
|||
FlightData,
|
||||
FlightDataPath,
|
||||
} from '../../server/app-render'
|
||||
import { matchSegment } from './match-segments'
|
||||
import { fetchServerResponse } from './app-router.client'
|
||||
|
||||
const fillCacheWithNewSubTreeData = (
|
||||
|
@ -15,6 +16,8 @@ const fillCacheWithNewSubTreeData = (
|
|||
const isLastEntry = flightDataPath.length <= 4
|
||||
const [parallelRouteKey, segment] = flightDataPath
|
||||
|
||||
const segmentForCache = Array.isArray(segment) ? segment[1] : segment
|
||||
|
||||
const existingChildSegmentMap =
|
||||
existingCache.parallelRoutes.get(parallelRouteKey)
|
||||
|
||||
|
@ -30,8 +33,8 @@ const fillCacheWithNewSubTreeData = (
|
|||
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap)
|
||||
}
|
||||
|
||||
const existingChildCacheNode = existingChildSegmentMap.get(segment)
|
||||
let childCacheNode = childSegmentMap.get(segment)
|
||||
const existingChildCacheNode = existingChildSegmentMap.get(segmentForCache)
|
||||
let childCacheNode = childSegmentMap.get(segmentForCache)
|
||||
|
||||
// In case of last segment start off the fetch at this level and don't copy further down.
|
||||
if (isLastEntry) {
|
||||
|
@ -40,7 +43,7 @@ const fillCacheWithNewSubTreeData = (
|
|||
!childCacheNode.data ||
|
||||
childCacheNode === existingChildCacheNode
|
||||
) {
|
||||
childSegmentMap.set(segment, {
|
||||
childSegmentMap.set(segmentForCache, {
|
||||
data: null,
|
||||
subTreeData: flightDataPath[3],
|
||||
parallelRoutes: new Map(),
|
||||
|
@ -61,7 +64,7 @@ const fillCacheWithNewSubTreeData = (
|
|||
subTreeData: childCacheNode.subTreeData,
|
||||
parallelRoutes: new Map(childCacheNode.parallelRoutes),
|
||||
}
|
||||
childSegmentMap.set(segment, childCacheNode)
|
||||
childSegmentMap.set(segmentForCache, childCacheNode)
|
||||
}
|
||||
|
||||
fillCacheWithNewSubTreeData(
|
||||
|
@ -164,7 +167,7 @@ const createOptimisticTree = (
|
|||
!flightRouterState || segment !== flightRouterState[0]
|
||||
|
||||
let parallelRoutes: FlightRouterState[1] = {}
|
||||
if (existingSegment === segment) {
|
||||
if (existingSegment !== null && matchSegment(existingSegment, segment)) {
|
||||
parallelRoutes = existingParallelRoutes
|
||||
}
|
||||
|
||||
|
@ -210,7 +213,7 @@ const walkTreeWithFlightDataPath = (
|
|||
|
||||
// Tree path returned from the server should always match up with the current tree in the browser
|
||||
// TODO: verify
|
||||
if (segment !== currentSegment) {
|
||||
if (!matchSegment(currentSegment, segment)) {
|
||||
throw new Error('SEGMENT MISMATCH')
|
||||
}
|
||||
|
||||
|
@ -316,6 +319,18 @@ export function reducer(
|
|||
// The with optimistic tree case only happens when the layouts have a loading state (loading.js)
|
||||
// The without optimistic tree case happens when there is no loading state, in that case we suspend in this reducer
|
||||
if (cacheType === 'hard') {
|
||||
if (
|
||||
mutable.patchedTree &&
|
||||
JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree)
|
||||
) {
|
||||
return {
|
||||
canonicalUrl: href,
|
||||
pushRef: { pendingPush, mpaNavigation: false },
|
||||
cache: cache,
|
||||
tree: mutable.patchedTree,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: flag on the tree of which part of the tree for if there is a loading boundary
|
||||
const isOptimistic = false
|
||||
|
||||
|
@ -336,10 +351,14 @@ export function reducer(
|
|||
cache,
|
||||
state.cache,
|
||||
segments.slice(1),
|
||||
() => fetchServerResponse(url, optimisticTree)
|
||||
() => {
|
||||
return fetchServerResponse(url, optimisticTree)
|
||||
}
|
||||
)
|
||||
|
||||
if (!res?.bailOptimistic) {
|
||||
mutable.previousTree = state.tree
|
||||
mutable.patchedTree = optimisticTree
|
||||
return {
|
||||
canonicalUrl: href,
|
||||
pushRef: { pendingPush, mpaNavigation: false },
|
||||
|
@ -349,18 +368,6 @@ export function reducer(
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
mutable.patchedTree &&
|
||||
JSON.stringify(mutable.previousTree) === JSON.stringify(state.tree)
|
||||
) {
|
||||
return {
|
||||
canonicalUrl: href,
|
||||
pushRef: { pendingPush, mpaNavigation: false },
|
||||
cache: cache,
|
||||
tree: mutable.patchedTree,
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.data) {
|
||||
cache.data = fetchServerResponse(url, state.tree)
|
||||
}
|
||||
|
@ -436,7 +443,12 @@ export function reducer(
|
|||
const treePath = flightDataPath.slice(0, -3)
|
||||
const [treePatch] = flightDataPath.slice(-2)
|
||||
|
||||
const newTree = walkTreeWithFlightDataPath(treePath, state.tree, treePatch)
|
||||
const newTree = walkTreeWithFlightDataPath(
|
||||
// TODO: remove ''
|
||||
['', ...treePath],
|
||||
state.tree,
|
||||
treePatch
|
||||
)
|
||||
|
||||
fillCacheWithNewSubTreeData(cache, state.cache, flightDataPath)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { tryGetPreviewData } from './api-utils/node'
|
|||
import { htmlEscapeJsonString } from './htmlescape'
|
||||
import { stripInternalQueries } from './utils'
|
||||
import { NextApiRequestCookies } from './api-utils'
|
||||
import { matchSegment } from '../client/components/match-segments'
|
||||
|
||||
const ReactDOMServer = process.env.__NEXT_REACT_ROOT
|
||||
? require('react-dom/server.browser')
|
||||
|
@ -245,6 +246,8 @@ function createServerComponentRenderer(
|
|||
return ServerComponentWrapper
|
||||
}
|
||||
|
||||
export type Segment = string | [param: string, value: string]
|
||||
|
||||
type LoaderTree = [
|
||||
segment: string,
|
||||
parallelRoutes: { [parallelRouterKey: string]: LoaderTree },
|
||||
|
@ -256,7 +259,7 @@ type LoaderTree = [
|
|||
]
|
||||
|
||||
export type FlightRouterState = [
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRoutes: { [parallelRouterKey: string]: FlightRouterState },
|
||||
url?: string,
|
||||
refresh?: 'refetch'
|
||||
|
@ -266,11 +269,11 @@ export type FlightSegmentPath =
|
|||
| any[]
|
||||
// Looks somewhat like this
|
||||
| [
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRouterKey: string,
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRouterKey: string,
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRouterKey: string
|
||||
]
|
||||
|
||||
|
@ -278,18 +281,21 @@ export type FlightDataPath =
|
|||
| any[]
|
||||
// Looks somewhat like this
|
||||
| [
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRoute: string,
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRoute: string,
|
||||
segment: string,
|
||||
segment: Segment,
|
||||
parallelRoute: string,
|
||||
tree: FlightRouterState,
|
||||
subTreeData: React.ReactNode
|
||||
]
|
||||
|
||||
export type FlightData = Array<FlightDataPath> | string
|
||||
export type ChildProp = { current: React.ReactNode; segment: string }
|
||||
export type ChildProp = {
|
||||
current: React.ReactNode
|
||||
segment: Segment
|
||||
}
|
||||
|
||||
export async function renderToHTML(
|
||||
req: IncomingMessage,
|
||||
|
@ -395,7 +401,7 @@ export async function renderToHTML(
|
|||
const dynamicParam = getDynamicParamFromSegment(segment)
|
||||
|
||||
const segmentTree: FlightRouterState = [
|
||||
dynamicParam ? dynamicParam.value : segment,
|
||||
dynamicParam ? [dynamicParam.param, dynamicParam.value] : segment,
|
||||
{},
|
||||
]
|
||||
|
||||
|
@ -459,7 +465,9 @@ export async function renderToHTML(
|
|||
}
|
||||
: parentParams
|
||||
|
||||
const actualSegment = segmentParam ? segmentParam.value : segment
|
||||
const actualSegment = segmentParam
|
||||
? [segmentParam.param, segmentParam.value]
|
||||
: segment
|
||||
|
||||
// This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down
|
||||
const parallelRouteComponents = Object.keys(parallelRoutes).reduce(
|
||||
|
@ -482,7 +490,7 @@ export async function renderToHTML(
|
|||
const childProp: ChildProp = {
|
||||
current: <ChildComponent />,
|
||||
segment: childSegmentParam
|
||||
? childSegmentParam.value
|
||||
? [childSegmentParam.param, childSegmentParam.value]
|
||||
: parallelRoutes[currentValue][0],
|
||||
}
|
||||
|
||||
|
@ -621,7 +629,9 @@ export async function renderToHTML(
|
|||
const parallelRoutesKeys = Object.keys(parallelRoutes)
|
||||
|
||||
const segmentParam = getDynamicParamFromSegment(segment)
|
||||
const actualSegment = segmentParam ? segmentParam.value : segment
|
||||
const actualSegment: Segment = segmentParam
|
||||
? [segmentParam.param, segmentParam.value]
|
||||
: segment
|
||||
|
||||
const currentParams = segmentParam
|
||||
? {
|
||||
|
@ -632,7 +642,7 @@ export async function renderToHTML(
|
|||
|
||||
const renderComponentsOnThisLevel =
|
||||
!flightRouterState ||
|
||||
actualSegment !== flightRouterState[0] ||
|
||||
!matchSegment(actualSegment, flightRouterState[0]) ||
|
||||
// Last item in the tree
|
||||
parallelRoutesKeys.length === 0 ||
|
||||
// Explicit refresh
|
||||
|
|
Loading…
Reference in a new issue