rsnext/packages/next/client/components/layout-router.client.tsx
Shu Ding 8a2bb49d6d
Fix failed to hydrate error with global CSS (#38626)
Temporarily we need to ensure that `<link>` tags are injected to `<head>` in the stream.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)
2022-07-14 10:26:25 +00:00

284 lines
8 KiB
TypeScript

import React, { useContext } from 'react'
import type { ChildProp } from '../../server/app-render'
import type { ChildSegmentMap } from '../../shared/lib/app-router-context'
import type {
FlightRouterState,
FlightSegmentPath,
FlightDataPath,
} from '../../server/app-render'
import {
AppTreeContext,
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) => matchSegment(val, b[i]))
}
function pathMatches(
flightDataPath: FlightDataPath,
layoutSegmentPath: FlightSegmentPath
): boolean {
// The last two items are the tree and subTreeData
const pathToLayout = flightDataPath.slice(0, -3)
return equalArray(layoutSegmentPath, pathToLayout)
}
function createInfinitePromise() {
if (!infinitePromise) {
infinitePromise = new Promise((/* resolve */) => {
// Note: this is used to debug when the rendering is never updated.
// setTimeout(() => {
// infinitePromise = new Error('Infinite promise')
// resolve()
// }, 5000)
})
}
return infinitePromise
}
export function InnerLayoutRouter({
parallelRouterKey,
url,
childNodes,
childProp,
segmentPath,
tree,
// isActive,
path,
}: {
parallelRouterKey: string
url: string
childNodes: ChildSegmentMap
childProp: ChildProp | null
segmentPath: FlightSegmentPath
tree: FlightRouterState
isActive: boolean
path: string
}) {
const { changeByServerResponse, tree: fullTree } =
useContext(FullAppTreeContext)
let childNode = childNodes.get(path)
if (childProp && !childNode) {
childNodes.set(path, {
data: null,
subTreeData: childProp.current,
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.
childNode = childNodes.get(path)
}
if (!childNode) {
const walkAddRefetch = (
segmentPathToWalk: FlightSegmentPath | undefined,
treeToRecreate: FlightRouterState
): FlightRouterState => {
if (segmentPathToWalk) {
const [segment, parallelRouteKey] = segmentPathToWalk
const isLast = segmentPathToWalk.length === 2
if (treeToRecreate[0] === segment) {
if (treeToRecreate[1].hasOwnProperty(parallelRouteKey)) {
if (isLast) {
const subTree = walkAddRefetch(
undefined,
treeToRecreate[1][parallelRouteKey]
)
if (!subTree[2]) {
subTree[2] = undefined
}
subTree[3] = 'refetch'
return [
treeToRecreate[0],
{
...treeToRecreate[1],
[parallelRouteKey]: [...subTree],
},
]
}
return [
treeToRecreate[0],
{
...treeToRecreate[1],
[parallelRouteKey]: walkAddRefetch(
segmentPathToWalk.slice(2),
treeToRecreate[1][parallelRouteKey]
),
},
]
}
}
}
return treeToRecreate
}
// TODO-APP: remove ''
const refetchTree = walkAddRefetch(['', ...segmentPath], fullTree)
const data = fetchServerResponse(new URL(url, location.origin), refetchTree)
childNodes.set(path, {
data,
subTreeData: null,
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)
}
// In the above case childNode was set on childNodes, so we have to get it from the cacheNodes again.
childNode = childNodes.get(path)
if (!childNode) {
throw new Error('Child node should always exist')
}
if (childNode.subTreeData && childNode.data) {
throw new Error('Child node should not have both subTreeData and data')
}
if (childNode.data) {
// TODO-APP: error case
const flightData = childNode.data.readRoot()
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
window.location.href = url
return null
}
let fastPath: boolean = false
// segmentPath matches what came back from the server. This is the happy path.
if (flightData.length === 1) {
const flightDataPath = flightData[0]
if (pathMatches(flightDataPath, segmentPath)) {
childNode.data = null
// Last item is the subtreeData
// TODO-APP: routerTreePatch needs to be applied to the tree, handle it in render?
const [, /* routerTreePatch */ subTreeData] = flightDataPath.slice(-2)
childNode.subTreeData = subTreeData
childNode.parallelRoutes = new Map()
fastPath = true
}
}
if (!fastPath) {
// For push we can set data in the cache
// segmentPath from the server does not match the layout's segmentPath
childNode.data = null
setTimeout(() => {
// @ts-ignore startTransition exists
React.startTransition(() => {
// TODO-APP: handle redirect
changeByServerResponse(fullTree, flightData)
})
})
// Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered.
throw createInfinitePromise()
}
}
// TODO-APP: double check users can't return null in a component that will kick in here
if (!childNode.subTreeData) {
throw createInfinitePromise()
}
return (
<AppTreeContext.Provider
value={{
tree: tree[1][parallelRouterKey],
childNodes: childNode.parallelRoutes,
// TODO-APP: overriding of url for parallel routes
url: url,
}}
>
{childNode.subTreeData}
</AppTreeContext.Provider>
)
}
function LoadingBoundary({
children,
loading,
}: {
children: React.ReactNode
loading?: React.ReactNode
}) {
if (loading) {
return <React.Suspense fallback={loading}>{children}</React.Suspense>
}
return <>{children}</>
}
export default function OuterLayoutRouter({
parallelRouterKey,
segmentPath,
childProp,
loading,
}: {
parallelRouterKey: string
segmentPath: FlightSegmentPath
childProp: ChildProp
loading: React.ReactNode | undefined
}) {
const { childNodes, tree, url } = useContext(AppTreeContext)
let childNodesForParallelRouter = childNodes.get(parallelRouterKey)
if (!childNodesForParallelRouter) {
childNodes.set(parallelRouterKey, new Map())
childNodesForParallelRouter = childNodes.get(parallelRouterKey)!
}
// This relates to the segments in the current router
// tree[1].children[0] refers to tree.children.segment in the data format
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 (
<>
{/* {stylesheets
? stylesheets.map((href) => (
<link rel="stylesheet" href={`/_next/${href}`} key={href} />
))
: null} */}
{preservedSegments.map((preservedSegment) => {
return (
<LoadingBoundary loading={loading} key={preservedSegment}>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
childProp={
childPropSegment === preservedSegment ? childProp : null
}
segmentPath={segmentPath}
path={preservedSegment}
isActive={currentChildSegment === preservedSegment}
/>
</LoadingBoundary>
)
})}
</>
)
}