Skip to content

Commit f93ae7c

Browse files
authored
fix TypeError edge-case for parallel slots rendered multiple times (#64271)
### What When rendering a parallel slot multiple times in a single layout, in conjunction with using an error boundary, the following TypeError is thrown: > Cannot destructure property 'parallelRouterKey' of 'param' as it is null ### Why I'm not 100% sure of the reason, but I believe this is because of how React attempts to dededupe (more specifically, "detriplficate") objects that it sees getting passed across the RSC -> client component boundary (and an error boundary is necessarily a client component). When React sees the same object twice, it'll create a reference to that object and then use that reference in future places where it sees the object. My assumption is that there's a bug somewhere here, as the `LayoutRouter` component for the subsequent duplicated parallel slots (after the first one) have no props, hence the TypeError. ### How Rather than passing the error component as a prop to `LayoutRouter`, this puts it as part of the `CacheNodeSeedData` data structure. This is more aligned with other properties anyway (such as `loading` and `rsc` for each segment), and seems to work around this bug as the `initialSeedData` prop is only passed from RSC->client once. EDIT: Confirmed this is also fixed after syncing the latest React, due to facebook/react#28669 Fixes #58485 Closes NEXT-2095
1 parent ddf2af5 commit f93ae7c

28 files changed

+218
-23
lines changed

packages/next/src/client/components/app-router.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export function createEmptyCacheNode(): CacheNode {
187187
parallelRoutes: new Map(),
188188
lazyDataResolved: false,
189189
loading: null,
190+
error: null,
190191
}
191192
}
192193

@@ -626,8 +627,9 @@ function Router({
626627
// Provided in AppTreeContext to ensure it can be overwritten in layout-router
627628
url: canonicalUrl,
628629
loading: cache.loading,
630+
error: cache.error,
629631
}
630-
}, [cache.parallelRoutes, tree, canonicalUrl, cache.loading])
632+
}, [cache.parallelRoutes, tree, canonicalUrl, cache.loading, cache.error])
631633

632634
const globalLayoutRouterContext = useMemo(() => {
633635
return {

packages/next/src/client/components/layout-router.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
FlightSegmentPath,
1010
Segment,
1111
} from '../../server/app-render/types'
12-
import type { ErrorComponent } from './error-boundary'
1312
import type { FocusAndScrollRef } from './router-reducer/router-reducer-types'
1413

1514
import React, {
@@ -358,6 +357,7 @@ function InnerLayoutRouter({
358357
parallelRoutes: new Map(),
359358
lazyDataResolved: false,
360359
loading: null,
360+
error: null,
361361
}
362362

363363
/**
@@ -453,6 +453,7 @@ function InnerLayoutRouter({
453453
// TODO-APP: overriding of url for parallel routes
454454
url: url,
455455
loading: childNode.loading,
456+
error: childNode.error,
456457
}}
457458
>
458459
{resolvedRsc}
@@ -507,9 +508,6 @@ function LoadingBoundary({
507508
export default function OuterLayoutRouter({
508509
parallelRouterKey,
509510
segmentPath,
510-
error,
511-
errorStyles,
512-
errorScripts,
513511
templateStyles,
514512
templateScripts,
515513
template,
@@ -519,9 +517,6 @@ export default function OuterLayoutRouter({
519517
}: {
520518
parallelRouterKey: string
521519
segmentPath: FlightSegmentPath
522-
error: ErrorComponent | undefined
523-
errorStyles: React.ReactNode | undefined
524-
errorScripts: React.ReactNode | undefined
525520
templateStyles: React.ReactNode | undefined
526521
templateScripts: React.ReactNode | undefined
527522
template: React.ReactNode
@@ -534,7 +529,7 @@ export default function OuterLayoutRouter({
534529
throw new Error('invariant expected layout router to be mounted')
535530
}
536531

537-
const { childNodes, tree, url, loading } = context
532+
const { childNodes, tree, url, loading, error } = context
538533

539534
// Get the current parallelRouter cache node
540535
let childNodesForParallelRouter = childNodes.get(parallelRouterKey)
@@ -580,9 +575,9 @@ export default function OuterLayoutRouter({
580575
value={
581576
<ScrollAndFocusHandler segmentPath={segmentPath}>
582577
<ErrorBoundary
583-
errorComponent={error}
584-
errorStyles={errorStyles}
585-
errorScripts={errorScripts}
578+
errorComponent={error?.[0]}
579+
errorStyles={error?.[1]}
580+
errorScripts={error?.[2]}
586581
>
587582
<LoadingBoundary
588583
hasLoading={Boolean(loading)}

packages/next/src/client/components/router-reducer/clear-cache-node-data-for-segment-path.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
2121
parallelRoutes: new Map(),
2222
lazyDataResolved: false,
2323
loading: null,
24+
error: null,
2425
}
2526
const existingCache: CacheNode = {
2627
lazyData: null,
@@ -30,6 +31,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
3031
prefetchHead: null,
3132
lazyDataResolved: false,
3233
loading: null,
34+
error: null,
3335
parallelRoutes: new Map([
3436
[
3537
'children',
@@ -44,6 +46,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
4446
head: null,
4547
prefetchHead: null,
4648
loading: null,
49+
error: null,
4750
parallelRoutes: new Map([
4851
[
4952
'children',
@@ -59,6 +62,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
5962
parallelRoutes: new Map(),
6063
lazyDataResolved: false,
6164
loading: null,
65+
error: null,
6266
},
6367
],
6468
]),
@@ -75,20 +79,23 @@ describe('clearCacheNodeDataForSegmentPath', () => {
7579

7680
expect(cache).toMatchInlineSnapshot(`
7781
{
82+
"error": null,
7883
"head": null,
7984
"lazyData": null,
8085
"lazyDataResolved": false,
8186
"loading": null,
8287
"parallelRoutes": Map {
8388
"children" => Map {
8489
"linking" => {
90+
"error": null,
8591
"head": null,
8692
"lazyData": null,
8793
"lazyDataResolved": false,
8894
"loading": null,
8995
"parallelRoutes": Map {
9096
"children" => Map {
9197
"" => {
98+
"error": null,
9299
"head": null,
93100
"lazyData": null,
94101
"lazyDataResolved": false,
@@ -109,6 +116,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
109116
</React.Fragment>,
110117
},
111118
"dashboard" => {
119+
"error": null,
112120
"head": null,
113121
"lazyData": null,
114122
"lazyDataResolved": false,

packages/next/src/client/components/router-reducer/clear-cache-node-data-for-segment-path.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function clearCacheNodeDataForSegmentPath(
4444
parallelRoutes: new Map(),
4545
lazyDataResolved: false,
4646
loading: null,
47+
error: null,
4748
})
4849
}
4950
return
@@ -61,6 +62,7 @@ export function clearCacheNodeDataForSegmentPath(
6162
parallelRoutes: new Map(),
6263
lazyDataResolved: false,
6364
loading: null,
65+
error: null,
6466
})
6567
}
6668
return
@@ -76,6 +78,7 @@ export function clearCacheNodeDataForSegmentPath(
7678
parallelRoutes: new Map(childCacheNode.parallelRoutes),
7779
lazyDataResolved: childCacheNode.lazyDataResolved,
7880
loading: childCacheNode.loading,
81+
error: childCacheNode.error,
7982
} as CacheNode
8083
childSegmentMap.set(cacheKey, childCacheNode)
8184
}

packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('createInitialRouterState', () => {
3737
buildId,
3838
initialTree,
3939
initialCanonicalUrl,
40-
initialSeedData: ['', {}, children, null],
40+
initialSeedData: ['', {}, children, null, null],
4141
initialParallelRoutes,
4242
location: new URL('/linking', 'https://localhost') as any,
4343
initialHead: <title>Test</title>,
@@ -48,7 +48,7 @@ describe('createInitialRouterState', () => {
4848
buildId,
4949
initialTree,
5050
initialCanonicalUrl,
51-
initialSeedData: ['', {}, children, null],
51+
initialSeedData: ['', {}, children, null, null],
5252
initialParallelRoutes,
5353
location: new URL('/linking', 'https://localhost') as any,
5454
initialHead: <title>Test</title>,
@@ -62,6 +62,7 @@ describe('createInitialRouterState', () => {
6262
prefetchHead: null,
6363
lazyDataResolved: false,
6464
loading: null,
65+
error: null,
6566
parallelRoutes: new Map([
6667
[
6768
'children',
@@ -81,6 +82,7 @@ describe('createInitialRouterState', () => {
8182
prefetchRsc: null,
8283
parallelRoutes: new Map(),
8384
loading: null,
85+
error: null,
8486
head: <title>Test</title>,
8587
prefetchHead: null,
8688
lazyDataResolved: false,
@@ -96,6 +98,7 @@ describe('createInitialRouterState', () => {
9698
prefetchHead: null,
9799
lazyDataResolved: false,
98100
loading: null,
101+
error: null,
99102
},
100103
],
101104
]),

packages/next/src/client/components/router-reducer/create-initial-router-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function createInitialRouterState({
4747
parallelRoutes: isServer ? new Map() : initialParallelRoutes,
4848
lazyDataResolved: false,
4949
loading: initialSeedData[3],
50+
error: initialSeedData[4],
5051
}
5152

5253
const canonicalUrl =

packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('fillCacheWithNewSubtreeData', () => {
3333
head: null,
3434
prefetchHead: null,
3535
loading: null,
36+
error: null,
3637
parallelRoutes: new Map(),
3738
lazyDataResolved: false,
3839
}
@@ -44,6 +45,7 @@ describe('fillCacheWithNewSubtreeData', () => {
4445
prefetchHead: null,
4546
lazyDataResolved: false,
4647
loading: null,
48+
error: null,
4749
parallelRoutes: new Map([
4850
[
4951
'children',
@@ -58,6 +60,7 @@ describe('fillCacheWithNewSubtreeData', () => {
5860
prefetchHead: null,
5961
lazyDataResolved: false,
6062
loading: null,
63+
error: null,
6164
parallelRoutes: new Map([
6265
[
6366
'children',
@@ -72,6 +75,7 @@ describe('fillCacheWithNewSubtreeData', () => {
7275
prefetchHead: null,
7376
lazyDataResolved: false,
7477
loading: null,
78+
error: null,
7579
parallelRoutes: new Map(),
7680
},
7781
],
@@ -104,6 +108,7 @@ describe('fillCacheWithNewSubtreeData', () => {
104108
prefetchHead: null,
105109
lazyDataResolved: false,
106110
loading: null,
111+
error: null,
107112
parallelRoutes: new Map([
108113
[
109114
'children',
@@ -118,6 +123,7 @@ describe('fillCacheWithNewSubtreeData', () => {
118123
prefetchHead: null,
119124
lazyDataResolved: false,
120125
loading: null,
126+
error: null,
121127
parallelRoutes: new Map([
122128
[
123129
'children',
@@ -133,6 +139,7 @@ describe('fillCacheWithNewSubtreeData', () => {
133139
prefetchHead: null,
134140
lazyDataResolved: false,
135141
loading: null,
142+
error: null,
136143
parallelRoutes: new Map(),
137144
},
138145
],
@@ -144,6 +151,7 @@ describe('fillCacheWithNewSubtreeData', () => {
144151
head: null,
145152
prefetchHead: null,
146153
loading: null,
154+
error: null,
147155
parallelRoutes: new Map([
148156
[
149157
'children',

packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ export function fillCacheWithNewSubTreeData(
4949
const seedData: CacheNodeSeedData = flightDataPath[3]
5050
const rsc = seedData[2]
5151
const loading = seedData[3]
52+
const error = seedData[4]
5253
childCacheNode = {
5354
lazyData: null,
5455
rsc,
5556
prefetchRsc: null,
5657
head: null,
5758
prefetchHead: null,
5859
loading,
60+
error,
5961
// Ensure segments other than the one we got data for are preserved.
6062
parallelRoutes: existingChildCacheNode
6163
? new Map(existingChildCacheNode.parallelRoutes)
@@ -101,6 +103,7 @@ export function fillCacheWithNewSubTreeData(
101103
parallelRoutes: new Map(childCacheNode.parallelRoutes),
102104
lazyDataResolved: false,
103105
loading: childCacheNode.loading,
106+
error: childCacheNode.error,
104107
} as CacheNode
105108
childSegmentMap.set(cacheKey, childCacheNode)
106109
}

packages/next/src/client/components/router-reducer/fill-lazy-items-till-leaf-with-head.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
4444
parallelRoutes: new Map(),
4545
lazyDataResolved: false,
4646
loading: null,
47+
error: null,
4748
}
4849
const existingCache: CacheNode = {
4950
lazyData: null,
@@ -53,6 +54,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
5354
prefetchHead: null,
5455
lazyDataResolved: false,
5556
loading: null,
57+
error: null,
5658
parallelRoutes: new Map([
5759
[
5860
'children',
@@ -67,6 +69,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
6769
prefetchHead: null,
6870
lazyDataResolved: false,
6971
loading: null,
72+
error: null,
7073
parallelRoutes: new Map([
7174
[
7275
'children',
@@ -81,6 +84,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
8184
prefetchHead: null,
8285
lazyDataResolved: false,
8386
loading: null,
87+
error: null,
8488
parallelRoutes: new Map(),
8589
},
8690
],
@@ -119,6 +123,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
119123
prefetchHead: null,
120124
lazyDataResolved: false,
121125
loading: null,
126+
error: null,
122127
parallelRoutes: new Map([
123128
[
124129
'children',
@@ -133,6 +138,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
133138
prefetchHead: null,
134139
lazyDataResolved: false,
135140
loading: null,
141+
error: null,
136142
parallelRoutes: new Map([
137143
[
138144
'children',
@@ -143,6 +149,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
143149
lazyData: null,
144150
lazyDataResolved: false,
145151
loading: null,
152+
error: null,
146153
parallelRoutes: new Map([
147154
[
148155
'children',
@@ -155,6 +162,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
155162
prefetchRsc: null,
156163
prefetchHead: null,
157164
loading: null,
165+
error: null,
158166
parallelRoutes: new Map(),
159167
lazyDataResolved: false,
160168
head: (
@@ -182,6 +190,7 @@ describe('fillLazyItemsTillLeafWithHead', () => {
182190
head: null,
183191
prefetchHead: null,
184192
loading: null,
193+
error: null,
185194
parallelRoutes: new Map(),
186195
lazyDataResolved: false,
187196
},

0 commit comments

Comments
 (0)