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 51c33de3f19bc..e4fd66204a528 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,32 +1,24 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
-import type { 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 type { PrefetchCacheEntry } from './router-reducer-types'
-import {
- getFlightDataPartsFromPath,
- isRootFlightDataPath,
-} from '../../flight-data-helpers'
+import type { NormalizedFlightData } from '../../flight-data-helpers'
export function applyFlightData(
existingCache: CacheNode,
cache: CacheNode,
- flightDataPath: FlightDataPath,
+ flightData: NormalizedFlightData,
prefetchEntry?: PrefetchCacheEntry
): boolean {
// The one before last item is the router state tree patch
- const {
- tree: treePatch,
- seedData,
- head,
- } = getFlightDataPartsFromPath(flightDataPath)
+ const { tree: treePatch, seedData, head, isRootRender } = flightData
// Handles case where prefetch only returns the router tree patch without rendered components.
if (seedData === null) {
return false
}
- if (isRootFlightDataPath(flightDataPath)) {
+ if (isRootRender) {
const rsc = seedData[1]
const loading = seedData[3]
cache.loading = loading
@@ -55,12 +47,7 @@ export function applyFlightData(
cache.parallelRoutes = new Map(existingCache.parallelRoutes)
cache.loading = existingCache.loading
// Create a copy of the existing cache with the rsc applied.
- fillCacheWithNewSubTreeData(
- cache,
- existingCache,
- flightDataPath,
- prefetchEntry
- )
+ fillCacheWithNewSubTreeData(cache, existingCache, flightData, prefetchEntry)
}
return true
diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
index d6277f66f03af..465349fdffdc2 100644
--- a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
+++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
@@ -32,11 +32,12 @@ export function createInitialRouterState({
// This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it
// as a URL that should be crawled.
const initialCanonicalUrl = initialCanonicalUrlParts.join('/')
+ const normalizedFlightData = getFlightDataPartsFromPath(initialFlightData[0])
const {
tree: initialTree,
seedData: initialSeedData,
head: initialHead,
- } = getFlightDataPartsFromPath(initialFlightData[0])
+ } = normalizedFlightData
const isServer = !location
// For the SSR render, seed data should always be available (we only send back a `null` response
// in the case of a `loading` segment, pre-PPR.)
@@ -114,7 +115,7 @@ export function createInitialRouterState({
createPrefetchCacheEntryForInitialLoad({
url,
data: {
- flightData: initialFlightData,
+ flightData: [normalizedFlightData],
canonicalUrl: undefined,
couldBeIntercepted: !!couldBeIntercepted,
// TODO: the server should probably send a value for this. Default to false for now.
diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts
index 9d3594082df58..3e0034ff935fc 100644
--- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts
+++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts
@@ -14,7 +14,6 @@ const { createFromFetch } = (
import type {
FlightRouterState,
NavigationFlightResponse,
- FetchServerResponseResult,
} from '../../../server/app-render/types'
import {
NEXT_ROUTER_PREFETCH_HEADER,
@@ -30,6 +29,10 @@ import {
import { callServer } from '../../app-call-server'
import { PrefetchKind } from './router-reducer-types'
import { hexHash } from '../../../shared/lib/hash'
+import {
+ normalizeFlightData,
+ type NormalizedFlightData,
+} from '../../flight-data-helpers'
export interface FetchServerResponseOptions {
readonly flightRouterState: FlightRouterState
@@ -39,6 +42,14 @@ export interface FetchServerResponseOptions {
readonly isHmrRefresh?: boolean
}
+export type FetchServerResponseResult = {
+ flightData: NormalizedFlightData[] | string
+ canonicalUrl: URL | undefined
+ couldBeIntercepted: boolean
+ isPrerender: boolean
+ postponed: boolean
+}
+
function urlToUrlWithoutFlightMarker(url: string): URL {
const urlWithoutFlightParameters = new URL(url, location.origin)
urlWithoutFlightParameters.searchParams.delete(NEXT_RSC_UNION_QUERY)
@@ -209,7 +220,7 @@ export async function fetchServerResponse(
}
return {
- flightData: response.f,
+ flightData: normalizeFlightData(response.f),
canonicalUrl: canonicalUrl,
couldBeIntercepted: interception,
isPrerender: isPrerender,
diff --git a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx
index 570cdcf00ba7e..26e4bcf54cb25 100644
--- a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx
+++ b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx
@@ -1,26 +1,19 @@
import React from 'react'
import { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data'
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
-import type { FlightData } from '../../../server/app-render/types'
+import type { NormalizedFlightData } from '../../flight-data-helpers'
-const getFlightData = (): FlightData => {
+const getFlightData = (): NormalizedFlightData[] => {
return [
- [
- 'children',
- 'linking',
- 'children',
- 'about',
- [
- 'about',
- {
- children: ['', {}],
- },
- ],
- ['about', {},
SubTreeData Injected!
],
- <>
- Head Injected!
- >,
- ],
+ {
+ pathToSegment: ['children', 'linking', 'children'],
+ segmentPath: ['children', 'linking', 'children', 'about'],
+ segment: 'about',
+ tree: ['about', { children: ['', {}] }],
+ seedData: ['about', SubTreeData Injected!
, {}, null],
+ head: 'Head Injected!',
+ isRootRender: false,
+ },
]
}
@@ -88,9 +81,9 @@ describe('fillCacheWithNewSubtreeData', () => {
}
// Mirrors the way router-reducer values are passed in.
- const flightDataPath = flightData[0]
+ const normalizedFlightData = flightData[0]
- fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
+ fillCacheWithNewSubTreeData(cache, existingCache, normalizedFlightData)
const expectedCache: CacheNode = {
lazyData: null,
diff --git a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts
index b107a92003f2f..79b67fb0c8257 100644
--- a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts
+++ b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts
@@ -1,15 +1,11 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
-import type { FlightDataPath } from '../../../server/app-render/types'
+import type { Segment } from '../../../server/app-render/types'
import { invalidateCacheByRouterState } from './invalidate-cache-by-router-state'
import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head'
import { createRouterCacheKey } from './create-router-cache-key'
import type { PrefetchCacheEntry } from './router-reducer-types'
import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'
-import {
- getFlightDataPartsFromPath,
- getNextFlightSegmentPath,
- isLastFlightDataPathEntry,
-} from '../../flight-data-helpers'
+import type { NormalizedFlightData } from '../../flight-data-helpers'
/**
* Common logic for filling cache with new sub tree data.
@@ -17,113 +13,119 @@ import {
function fillCacheHelper(
newCache: CacheNode,
existingCache: CacheNode,
- flightDataPath: FlightDataPath,
+ flightData: NormalizedFlightData,
prefetchEntry: PrefetchCacheEntry | undefined,
fillLazyItems: boolean
): void {
- const isLastEntry = isLastFlightDataPathEntry(flightDataPath)
- const [parallelRouteKey, segment] = flightDataPath
+ const {
+ segmentPath,
+ seedData: cacheNodeSeedData,
+ tree: treePatch,
+ head,
+ } = flightData
+ let newCacheNode = newCache
+ let existingCacheNode = existingCache
- const cacheKey = createRouterCacheKey(segment)
+ for (let i = 0; i < segmentPath.length; i += 2) {
+ const parallelRouteKey: string = segmentPath[i]
+ const segment: Segment = segmentPath[i + 1]
- const existingChildSegmentMap =
- existingCache.parallelRoutes.get(parallelRouteKey)
+ // segmentPath is a repeating tuple of parallelRouteKey and segment
+ // we know we've hit the last entry we've reached our final pair
+ const isLastEntry = i === segmentPath.length - 2
+ const cacheKey = createRouterCacheKey(segment)
- if (!existingChildSegmentMap) {
- // Bailout because the existing cache does not have the path to the leaf node
- // Will trigger lazy fetch in layout-router because of missing segment
- return
- }
+ const existingChildSegmentMap =
+ existingCacheNode.parallelRoutes.get(parallelRouteKey)
- let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey)
- if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
- childSegmentMap = new Map(existingChildSegmentMap)
- newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap)
- }
+ if (!existingChildSegmentMap) {
+ // Bailout because the existing cache does not have the path to the leaf node
+ // Will trigger lazy fetch in layout-router because of missing segment
+ continue
+ }
- const existingChildCacheNode = existingChildSegmentMap.get(cacheKey)
- let childCacheNode = childSegmentMap.get(cacheKey)
- const {
- seedData: cacheNodeSeedData,
- tree: treePatch,
- head,
- } = getFlightDataPartsFromPath(flightDataPath)
-
- if (isLastEntry) {
- if (
- cacheNodeSeedData &&
- (!childCacheNode ||
- !childCacheNode.lazyData ||
- childCacheNode === existingChildCacheNode)
- ) {
- const incomingSegment = cacheNodeSeedData[0]
- const rsc = cacheNodeSeedData[1]
- const loading = cacheNodeSeedData[3]
+ let childSegmentMap = newCacheNode.parallelRoutes.get(parallelRouteKey)
+ if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
+ childSegmentMap = new Map(existingChildSegmentMap)
+ newCacheNode.parallelRoutes.set(parallelRouteKey, childSegmentMap)
+ }
- childCacheNode = {
- lazyData: null,
- // When `fillLazyItems` is false, we only want to fill the RSC data for the layout,
- // not the page segment.
- rsc: fillLazyItems || incomingSegment !== PAGE_SEGMENT_KEY ? rsc : null,
- prefetchRsc: null,
- head: null,
- prefetchHead: null,
- loading,
- parallelRoutes:
- fillLazyItems && existingChildCacheNode
- ? new Map(existingChildCacheNode.parallelRoutes)
- : new Map(),
- }
+ const existingChildCacheNode = existingChildSegmentMap.get(cacheKey)
+ let childCacheNode = childSegmentMap.get(cacheKey)
- if (existingChildCacheNode && fillLazyItems) {
- invalidateCacheByRouterState(
- childCacheNode,
- existingChildCacheNode,
- treePatch
- )
- }
- if (fillLazyItems) {
- fillLazyItemsTillLeafWithHead(
- childCacheNode,
- existingChildCacheNode,
- treePatch,
- cacheNodeSeedData,
- head,
- prefetchEntry
- )
+ if (isLastEntry) {
+ if (
+ cacheNodeSeedData &&
+ (!childCacheNode ||
+ !childCacheNode.lazyData ||
+ childCacheNode === existingChildCacheNode)
+ ) {
+ const incomingSegment = cacheNodeSeedData[0]
+ const rsc = cacheNodeSeedData[1]
+ const loading = cacheNodeSeedData[3]
+
+ childCacheNode = {
+ lazyData: null,
+ // When `fillLazyItems` is false, we only want to fill the RSC data for the layout,
+ // not the page segment.
+ rsc:
+ fillLazyItems || incomingSegment !== PAGE_SEGMENT_KEY ? rsc : null,
+ prefetchRsc: null,
+ head: null,
+ prefetchHead: null,
+ loading,
+ parallelRoutes:
+ fillLazyItems && existingChildCacheNode
+ ? new Map(existingChildCacheNode.parallelRoutes)
+ : new Map(),
+ }
+
+ if (existingChildCacheNode && fillLazyItems) {
+ invalidateCacheByRouterState(
+ childCacheNode,
+ existingChildCacheNode,
+ treePatch
+ )
+ }
+ if (fillLazyItems) {
+ fillLazyItemsTillLeafWithHead(
+ childCacheNode,
+ existingChildCacheNode,
+ treePatch,
+ cacheNodeSeedData,
+ head,
+ prefetchEntry
+ )
+ }
+
+ childSegmentMap.set(cacheKey, childCacheNode)
}
+ continue
+ }
- childSegmentMap.set(cacheKey, childCacheNode)
+ if (!childCacheNode || !existingChildCacheNode) {
+ // Bailout because the existing cache does not have the path to the leaf node
+ // Will trigger lazy fetch in layout-router because of missing segment
+ continue
}
- return
- }
- if (!childCacheNode || !existingChildCacheNode) {
- // Bailout because the existing cache does not have the path to the leaf node
- // Will trigger lazy fetch in layout-router because of missing segment
- return
- }
+ if (childCacheNode === existingChildCacheNode) {
+ childCacheNode = {
+ lazyData: childCacheNode.lazyData,
+ rsc: childCacheNode.rsc,
+ prefetchRsc: childCacheNode.prefetchRsc,
+ head: childCacheNode.head,
+ prefetchHead: childCacheNode.prefetchHead,
+ parallelRoutes: new Map(childCacheNode.parallelRoutes),
+ loading: childCacheNode.loading,
+ } as CacheNode
+ childSegmentMap.set(cacheKey, childCacheNode)
+ }
- if (childCacheNode === existingChildCacheNode) {
- childCacheNode = {
- lazyData: childCacheNode.lazyData,
- rsc: childCacheNode.rsc,
- prefetchRsc: childCacheNode.prefetchRsc,
- head: childCacheNode.head,
- prefetchHead: childCacheNode.prefetchHead,
- parallelRoutes: new Map(childCacheNode.parallelRoutes),
- loading: childCacheNode.loading,
- } as CacheNode
- childSegmentMap.set(cacheKey, childCacheNode)
+ // Move deeper into the cache nodes
+ newCacheNode = childCacheNode
+ existingCacheNode = existingChildCacheNode
}
-
- fillCacheHelper(
- childCacheNode,
- existingChildCacheNode,
- getNextFlightSegmentPath(flightDataPath),
- prefetchEntry,
- fillLazyItems
- )
}
/**
@@ -132,17 +134,17 @@ function fillCacheHelper(
export function fillCacheWithNewSubTreeData(
newCache: CacheNode,
existingCache: CacheNode,
- flightDataPath: FlightDataPath,
+ flightData: NormalizedFlightData,
prefetchEntry?: PrefetchCacheEntry
): void {
- fillCacheHelper(newCache, existingCache, flightDataPath, prefetchEntry, true)
+ fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, true)
}
export function fillCacheWithNewSubTreeDataButOnlyLoading(
newCache: CacheNode,
existingCache: CacheNode,
- flightDataPath: FlightDataPath,
+ flightData: NormalizedFlightData,
prefetchEntry?: PrefetchCacheEntry
): void {
- fillCacheHelper(newCache, existingCache, flightDataPath, prefetchEntry, false)
+ fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, false)
}
diff --git a/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx b/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx
index 733cd38d86358..f55e0d28746eb 100644
--- a/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx
+++ b/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx
@@ -1,27 +1,20 @@
import React from 'react'
-import type { FlightData } from '../../../server/app-render/types'
import { invalidateCacheBelowFlightSegmentPath } from './invalidate-cache-below-flight-segmentpath'
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
import { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data'
+import type { NormalizedFlightData } from '../../flight-data-helpers'
-const getFlightData = (): FlightData => {
+const getFlightData = (): NormalizedFlightData[] => {
return [
- [
- 'children',
- 'linking',
- 'children',
- 'about',
- [
- 'about',
- {
- children: ['', {}],
- },
- ],
- ['about', {}, About Page!
],
- <>
- About page!
- >,
- ],
+ {
+ pathToSegment: ['children', 'linking', 'children'],
+ segmentPath: ['children', 'linking', 'children', 'about'],
+ segment: 'about',
+ tree: ['about', { children: ['', {}] }],
+ seedData: ['about', About Page!
, {}, null],
+ head: 'About page!',
+ isRootRender: false,
+ },
]
}
@@ -89,20 +82,19 @@ describe('invalidateCacheBelowFlightSegmentPath', () => {
}
// Mirrors the way router-reducer values are passed in.
- const flightDataPath = flightData[0]
- const flightSegmentPath = flightDataPath.slice(0, -3)
+ const normalizedFlightData = flightData[0]
// Copy rsc for the root node of the cache.
cache.rsc = existingCache.rsc
cache.prefetchRsc = existingCache.prefetchRsc
// Create a copy of the existing cache with the rsc applied.
- fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath)
+ fillCacheWithNewSubTreeData(cache, existingCache, normalizedFlightData)
// Invalidate the cache below the flight segment path. This should remove the 'about' node.
invalidateCacheBelowFlightSegmentPath(
cache,
existingCache,
- flightSegmentPath
+ normalizedFlightData.segmentPath
)
const expectedCache: CacheNode = {
diff --git a/packages/next/src/client/components/router-reducer/ppr-navigations.ts b/packages/next/src/client/components/router-reducer/ppr-navigations.ts
index 770db0425a355..821bcd3e31764 100644
--- a/packages/next/src/client/components/router-reducer/ppr-navigations.ts
+++ b/packages/next/src/client/components/router-reducer/ppr-navigations.ts
@@ -3,7 +3,6 @@ import type {
FlightRouterState,
FlightSegmentPath,
Segment,
- FetchServerResponseResult,
} from '../../../server/app-render/types'
import type {
CacheNode,
@@ -14,9 +13,9 @@ import {
DEFAULT_SEGMENT_KEY,
PAGE_SEGMENT_KEY,
} from '../../../shared/lib/segment'
-import { getFlightDataPartsFromPath } from '../../flight-data-helpers'
import { matchSegment } from '../match-segments'
import { createRouterCacheKey } from './create-router-cache-key'
+import type { FetchServerResponseResult } from './fetch-server-response'
// This is yet another tree type that is used to track pending promises that
// need to be fulfilled once the dynamic data is received. The terminal nodes of
@@ -355,8 +354,8 @@ export function listenForDynamicRequest(
) {
responsePromise.then(
({ flightData }: FetchServerResponseResult) => {
- for (const flightDataPath of flightData) {
- if (typeof flightDataPath === 'string') {
+ for (const normalizedFlightData of flightData) {
+ if (typeof normalizedFlightData === 'string') {
// Happens when navigating to page in `pages` from `app`. We shouldn't
// get here because should have already handled this during
// the prefetch.
@@ -368,7 +367,7 @@ export function listenForDynamicRequest(
tree: serverRouterState,
seedData: dynamicData,
head: dynamicHead,
- } = getFlightDataPartsFromPath(flightDataPath)
+ } = normalizedFlightData
if (!dynamicData) {
// This shouldn't happen. PPR should always send back a response.
diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts
index 113e011e056ca..6a9eedde895df 100644
--- a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts
+++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts
@@ -1,4 +1,7 @@
-import { fetchServerResponse } from './fetch-server-response'
+import {
+ fetchServerResponse,
+ type FetchServerResponseResult,
+} from './fetch-server-response'
import {
PrefetchCacheEntryStatus,
type PrefetchCacheEntry,
@@ -6,7 +9,6 @@ import {
type ReadonlyReducerState,
} from './router-reducer-types'
import { prefetchQueue } from './reducers/prefetch-reducer'
-import type { FetchServerResponseResult } from '../../../server/app-render/types'
const INTERCEPTION_CACHE_KEY_MARKER = '%'
diff --git a/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts
index 17eaa3b7abff0..b5474794bf1ce 100644
--- a/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts
+++ b/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts
@@ -15,10 +15,6 @@ import type { CacheNode } from '../../../../shared/lib/app-router-context.shared
import { createEmptyCacheNode } from '../../app-router'
import { handleSegmentMismatch } from '../handle-segment-mismatch'
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree'
-import {
- getFlightDataPartsFromPath,
- isRootFlightDataPath,
-} from '../../../flight-data-helpers'
// A version of refresh reducer that keeps the cache around instead of wiping all of it.
function hmrRefreshReducerImpl(
@@ -63,15 +59,14 @@ function hmrRefreshReducerImpl(
let currentTree = state.tree
let currentCache = state.cache
- for (const flightDataPath of flightData) {
- if (!isRootFlightDataPath(flightDataPath)) {
+ for (const normalizedFlightData of flightData) {
+ const { tree: treePatch, isRootRender } = normalizedFlightData
+ if (!isRootRender) {
// TODO-APP: handle this case better
console.log('REFRESH FAILED')
return state
}
- const { tree: treePatch } = getFlightDataPartsFromPath(flightDataPath)
-
const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
[''],
@@ -100,7 +95,11 @@ function hmrRefreshReducerImpl(
if (canonicalUrlOverride) {
mutable.canonicalUrl = canonicalUrlOverrideHref
}
- const applied = applyFlightData(currentCache, cache, flightDataPath)
+ const applied = applyFlightData(
+ currentCache,
+ cache,
+ normalizedFlightData
+ )
if (applied) {
mutable.cache = cache
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 73e6767f82c2d..a0295f5178483 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
@@ -31,10 +31,6 @@ import {
} from '../prefetch-cache-utils'
import { clearCacheNodeDataForSegmentPath } from '../clear-cache-node-data-for-segment-path'
import { fillCacheWithNewSubTreeDataButOnlyLoading } from '../fill-cache-with-new-subtree-data'
-import {
- getFlightDataPartsFromPath,
- isRootFlightDataPath,
-} from '../../../flight-data-helpers'
export function handleExternalUrl(
state: ReadonlyReducerState,
@@ -171,13 +167,14 @@ export function navigateReducer(
let currentTree = state.tree
let currentCache = state.cache
let scrollableSegments: FlightSegmentPath[] = []
- for (const flightDataPath of flightData) {
+ for (const normalizedFlightData of flightData) {
const {
tree: treePatch,
pathToSegment: flightSegmentPath,
seedData,
head,
- } = getFlightDataPartsFromPath(flightDataPath)
+ isRootRender,
+ } = normalizedFlightData
// TODO-APP: remove ''
const flightSegmentPathWithLeadingEmpty = ['', ...flightSegmentPath]
@@ -217,7 +214,7 @@ export function navigateReducer(
// via updateCacheNodeOnNavigation. The current structure is just
// an incremental step.
seedData &&
- isRootFlightDataPath(flightDataPath) &&
+ isRootRender &&
!prefetchValues.aliased &&
postponed
) {
@@ -295,8 +292,7 @@ export function navigateReducer(
// The prefetch cache entry was aliased -- this signals that we only fill in the cache with the
// loading state and not the actual parallel route seed data.
if (prefetchValues.aliased && seedData) {
- // Root render
- if (isRootFlightDataPath(flightDataPath)) {
+ if (isRootRender) {
// Fill in the cache with the new loading / rsc data
const rsc = seedData[1]
const loading = seedData[3]
@@ -313,7 +309,7 @@ export function navigateReducer(
fillCacheWithNewSubTreeDataButOnlyLoading(
cache,
currentCache,
- flightDataPath,
+ normalizedFlightData,
prefetchValues
)
}
@@ -343,7 +339,7 @@ export function navigateReducer(
applied = applyFlightData(
currentCache,
cache,
- flightDataPath,
+ normalizedFlightData,
prefetchValues
)
}
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 73c518166d58a..6c8ea5c99bbc8 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
@@ -16,10 +16,6 @@ import { createEmptyCacheNode } from '../../app-router'
import { handleSegmentMismatch } from '../handle-segment-mismatch'
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree'
import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments'
-import {
- getFlightDataPartsFromPath,
- isRootFlightDataPath,
-} from '../../../flight-data-helpers'
export function refreshReducer(
state: ReadonlyReducerState,
@@ -67,18 +63,19 @@ export function refreshReducer(
// Remove cache.lazyData as it has been resolved at this point.
cache.lazyData = null
- for (const flightDataPath of flightData) {
- if (!isRootFlightDataPath(flightDataPath)) {
- // TODO-APP: handle this case better
- console.log('REFRESH FAILED')
- return state
- }
-
+ for (const normalizedFlightData of flightData) {
const {
tree: treePatch,
seedData: cacheNodeSeedData,
head,
- } = getFlightDataPartsFromPath(flightDataPath)
+ isRootRender,
+ } = normalizedFlightData
+
+ if (!isRootRender) {
+ // TODO-APP: handle this case better
+ console.log('REFRESH FAILED')
+ return state
+ }
const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
index 8809d083bc798..9b49a5f6ee51f 100644
--- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
+++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
@@ -1,7 +1,6 @@
import type {
ActionFlightResponse,
ActionResult,
- FlightData,
} from '../../../../server/app-render/types'
import { callServer } from '../../../app-call-server'
import {
@@ -41,14 +40,14 @@ import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-c
import { handleSegmentMismatch } from '../handle-segment-mismatch'
import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments'
import {
- getFlightDataPartsFromPath,
- isRootFlightDataPath,
+ normalizeFlightData,
+ type NormalizedFlightData,
} from '../../../flight-data-helpers'
type FetchServerActionResult = {
redirectLocation: URL | undefined
actionResult?: ActionResult
- actionFlightData?: FlightData | undefined | null
+ actionFlightData?: NormalizedFlightData[] | string
revalidatedParts: {
tag: boolean
cookie: boolean
@@ -125,7 +124,7 @@ async function fetchServerAction(
if (location) {
// if it was a redirection, then result is just a regular RSC payload
return {
- actionFlightData: response.f,
+ actionFlightData: normalizeFlightData(response.f),
redirectLocation,
revalidatedParts,
}
@@ -133,7 +132,7 @@ async function fetchServerAction(
return {
actionResult: response.a,
- actionFlightData: response.f,
+ actionFlightData: normalizeFlightData(response.f),
redirectLocation,
revalidatedParts,
}
@@ -225,18 +224,19 @@ export function serverActionReducer(
mutable.canonicalUrl = newHref
}
- for (const flightDataPath of flightData) {
- if (!isRootFlightDataPath(flightDataPath)) {
- // TODO-APP: handle this case better
- console.log('SERVER ACTION APPLY FAILED')
- return state
- }
-
+ for (const normalizedFlightData of flightData) {
const {
tree: treePatch,
seedData: cacheNodeSeedData,
head,
- } = getFlightDataPartsFromPath(flightDataPath)
+ isRootRender,
+ } = normalizedFlightData
+
+ if (!isRootRender) {
+ // TODO-APP: handle this case better
+ console.log('SERVER ACTION APPLY FAILED')
+ return state
+ }
// Given the path can only have two items the items are only the router state and rsc for the root.
const newTree = applyRouterStatePatchToTree(
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 866abc5a0e647..9238c748c29f7 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
@@ -13,7 +13,6 @@ import { handleMutable } from '../handle-mutable'
import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime'
import { createEmptyCacheNode } from '../../app-router'
import { handleSegmentMismatch } from '../handle-segment-mismatch'
-import { getFlightDataPartsFromPath } from '../../../flight-data-helpers'
export function serverPatchReducer(
state: ReadonlyReducerState,
@@ -40,9 +39,9 @@ export function serverPatchReducer(
let currentTree = state.tree
let currentCache = state.cache
- for (const flightDataPath of flightData) {
+ for (const normalizedFlightData of flightData) {
const { segmentPath: flightSegmentPath, tree: treePatch } =
- getFlightDataPartsFromPath(flightDataPath)
+ normalizedFlightData
const newTree = applyRouterStatePatchToTree(
// TODO-APP: remove ''
@@ -74,7 +73,7 @@ export function serverPatchReducer(
}
const cache: CacheNode = createEmptyCacheNode()
- applyFlightData(currentCache, cache, flightDataPath)
+ applyFlightData(currentCache, cache, normalizedFlightData)
mutable.patchedTree = newTree
mutable.cache = cache
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 909b7afbc3b52..0df6b9276b98e 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
@@ -2,8 +2,8 @@ import type { CacheNode } from '../../../shared/lib/app-router-context.shared-ru
import type {
FlightRouterState,
FlightSegmentPath,
- FetchServerResponseResult,
} from '../../../server/app-render/types'
+import type { FetchServerResponseResult } from './fetch-server-response'
export const ACTION_REFRESH = 'refresh'
export const ACTION_NAVIGATE = 'navigate'
diff --git a/packages/next/src/client/flight-data-helpers.ts b/packages/next/src/client/flight-data-helpers.ts
index a16a16deee68f..01fa57faff42b 100644
--- a/packages/next/src/client/flight-data-helpers.ts
+++ b/packages/next/src/client/flight-data-helpers.ts
@@ -1,12 +1,13 @@
import type {
CacheNodeSeedData,
+ FlightData,
FlightDataPath,
FlightRouterState,
FlightSegmentPath,
Segment,
} from '../server/app-render/types'
-export function getFlightDataPartsFromPath(flightDataPath: FlightDataPath): {
+export type NormalizedFlightData = {
/**
* The full `FlightSegmentPath` inclusive of the final `Segment`
*/
@@ -19,7 +20,16 @@ export function getFlightDataPartsFromPath(flightDataPath: FlightDataPath): {
tree: FlightRouterState
seedData: CacheNodeSeedData | null
head: React.ReactNode | null
-} {
+ isRootRender: boolean
+}
+
+// TODO: We should only have to export `normalizeFlightData`, however because the initial flight data
+// that gets passed to `createInitialRouterState` doesn't conform to the `FlightDataPath` type (it's missing the root segment)
+// we're currently exporting it so we can use it directly. This should be fixed as part of the unification of
+// the different ways we express `FlightSegmentPath`.
+export function getFlightDataPartsFromPath(
+ flightDataPath: FlightDataPath
+): NormalizedFlightData {
// tree, seedData, and head are *always* the last three items in the `FlightDataPath`.
const [tree, seedData, head] = flightDataPath.slice(-3)
// The `FlightSegmentPath` is everything except the last three items. For a root render, it won't be present.
@@ -37,19 +47,10 @@ export function getFlightDataPartsFromPath(flightDataPath: FlightDataPath): {
tree,
seedData,
head,
+ isRootRender: flightDataPath.length === 3,
}
}
-export function isRootFlightDataPath(flightDataPath: FlightDataPath): boolean {
- return flightDataPath.length === 3
-}
-
-export function isLastFlightDataPathEntry(
- flightDataPath: FlightDataPath
-): boolean {
- return flightDataPath.length === 5
-}
-
export function getNextFlightSegmentPath(
flightSegmentPath: FlightSegmentPath
): FlightSegmentPath {
@@ -57,3 +58,15 @@ export function getNextFlightSegmentPath(
// to get the next segment path.
return flightSegmentPath.slice(2)
}
+
+export function normalizeFlightData(
+ flightData: FlightData
+): NormalizedFlightData[] | string {
+ // FlightData can be a string when the server didn't respond with a proper flight response,
+ // or when a redirect happens, to signal to the client that it needs to perform an MPA navigation.
+ if (typeof flightData === 'string') {
+ return flightData
+ }
+
+ return flightData.map(getFlightDataPartsFromPath)
+}
diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index 343378aec0a38..7db370c906be9 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -377,7 +377,12 @@ async function generateDynamicRSCPayload(
): Promise {
// Flight data that is going to be passed to the browser.
// Currently a single item array but in the future multiple patches might be combined in a single request.
- let flightData: FlightData | null = null
+
+ // We initialize `flightData` to an empty string because the client router knows how to tolerate
+ // it (treating it as an MPA navigation). The only time this function wouldn't generate flight data
+ // is for server actions, if the server action handler instructs this function to skip it. When the server
+ // action reducer sees a falsy value, it'll simply resolve the action with no data.
+ let flightData: FlightData = ''
const {
componentMod: {
@@ -450,12 +455,7 @@ async function generateDynamicRSCPayload(
// Otherwise, it's a regular RSC response.
return {
b: ctx.renderOpts.buildId,
- // Anything besides an action response should have non-null flightData.
- // We don't ever expect this to be null because `skipFlight` is only
- // used when invoked by a server action, which is covered above.
- // The client router can handle an empty string (treating it as an MPA navigation),
- // so we'll use that as a fallback.
- f: flightData ?? '',
+ f: flightData,
}
}
@@ -703,7 +703,7 @@ async function getErrorRSCPayload(
f: [[initialTree, initialSeedData, initialHead]],
G: GlobalError,
s: typeof ctx.renderOpts.postponed === 'string',
- } satisfies RSCPayload
+ } satisfies InitialRSCPayload
}
// This component must run in an SSR context. It will render the RSC root component
diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts
index 81f6913eb91ec..c162ff66f52b3 100644
--- a/packages/next/src/server/app-render/types.ts
+++ b/packages/next/src/server/app-render/types.ts
@@ -240,15 +240,7 @@ export type ActionFlightResponse = {
/** buildId */
b: string
/** flightData */
- f: FlightData | null
-}
-
-export type FetchServerResponseResult = {
- flightData: FlightData
- canonicalUrl: URL | undefined
- couldBeIntercepted: boolean
- isPrerender: boolean
- postponed: boolean
+ f: FlightData
}
export type RSCPayload =
diff --git a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts
index b6de5bd741481..38dd9a7ac16ff 100644
--- a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts
+++ b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts
@@ -1,14 +1,12 @@
'use client'
+import type { FetchServerResponseResult } from '../../client/components/router-reducer/fetch-server-response'
import type {
FocusAndScrollRef,
PrefetchKind,
RouterChangeByServerResponse,
} from '../../client/components/router-reducer/router-reducer-types'
-import type {
- FlightRouterState,
- FetchServerResponseResult,
-} from '../../server/app-render/types'
+import type { FlightRouterState } from '../../server/app-render/types'
import React from 'react'
export type ChildSegmentMap = Map