Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of PPR client navigations #59725

Merged
merged 4 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
useCallback,
startTransition,
useInsertionEffect,
useDeferredValue,
} from 'react'
import {
AppRouterContext,
Expand Down Expand Up @@ -230,6 +231,31 @@ function copyNextJsInternalHistoryState(data: any) {
return data
}

function Head({
headCacheNode,
}: {
headCacheNode: CacheNode | null
}): React.ReactNode {
// If this segment has a `prefetchHead`, it's the statically prefetched data.
// We should use that on initial render instead of `head`. Then we'll switch
// to `head` when the dynamic response streams in.
const head = headCacheNode !== null ? headCacheNode.head : null
const prefetchHead =
headCacheNode !== null ? headCacheNode.prefetchHead : null

// If no prefetch data is available, then we go straight to rendering `head`.
const resolvedPrefetchRsc = prefetchHead !== null ? prefetchHead : head

// We use `useDeferredValue` to handle switching between the prefetched and
// final values. The second argument is returned on initial render, then it
// re-renders with the first argument.
//
// @ts-expect-error The second argument to `useDeferredValue` is only
// available in the experimental builds. When its disabled, it will always
// return `head`.
return useDeferredValue(head, resolvedPrefetchRsc)
}

/**
* The global router that wraps the application components.
*/
Expand Down Expand Up @@ -542,10 +568,24 @@ function Router({
const { cache, tree, nextUrl, focusAndScrollRef } =
useUnwrapState(reducerState)

const head = useMemo(() => {
const matchingHead = useMemo(() => {
return findHeadInCache(cache, tree[1])
}, [cache, tree])

let head
if (matchingHead !== null) {
// The head is wrapped in an extra component so we can use
// `useDeferredValue` to swap between the prefetched and final versions of
// the head. (This is what LayoutRouter does for segment data, too.)
//
// The `key` is used to remount the component whenever the head moves to
// a different segment.
const [headCacheNode, headKey] = matchingHead
head = <Head key={headKey} headCacheNode={headCacheNode} />
} else {
head = null
}

let content = (
<RedirectBoundary>
{head}
Expand Down
38 changes: 31 additions & 7 deletions packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import type {
import type { ErrorComponent } from './error-boundary'
import type { FocusAndScrollRef } from './router-reducer/router-reducer-types'

import React, { useContext, use, startTransition, Suspense } from 'react'
import React, {
useContext,
use,
startTransition,
Suspense,
useDeferredValue,
} from 'react'
import ReactDOM from 'react-dom'
import {
LayoutRouterContext,
Expand Down Expand Up @@ -358,12 +364,30 @@ function InnerLayoutRouter({
childNodes.set(cacheKey, newLazyCacheNode)
}

// `rsc` represents the renderable node for this segment. It's either a
// React node or a promise for a React node, except we special case `null` to
// represent that this segment's data is missing. If it's a promise, we need
// to unwrap it so we can determine whether or not the data is missing.
const rsc: any = childNode.rsc
const resolvedRsc =
// `rsc` represents the renderable node for this segment.

// If this segment has a `prefetchRsc`, it's the statically prefetched data.
// We should use that on initial render instead of `rsc`. Then we'll switch
// to `rsc` when the dynamic response streams in.
//
// If no prefetch data is available, then we go straight to rendering `rsc`.
const resolvedPrefetchRsc =
childNode.prefetchRsc !== null ? childNode.prefetchRsc : childNode.rsc

// We use `useDeferredValue` to handle switching between the prefetched and
// final values. The second argument is returned on initial render, then it
// re-renders with the first argument.
//
// @ts-expect-error The second argument to `useDeferredValue` is only
// available in the experimental builds. When its disabled, it will always
// return `rsc`.
const rsc: any = useDeferredValue(childNode.rsc, resolvedPrefetchRsc)

// `rsc` is either a React node or a promise for a React node, except we
// special case `null` to represent that this segment's data is missing. If
// it's a promise, we need to unwrap it so we can determine whether or not the
// data is missing.
const resolvedRsc: React.ReactNode =
typeof rsc === 'object' && rsc !== null && typeof rsc.then === 'function'
? use(rsc)
: rsc
Expand Down
Loading