2156
return id;
2157
}
2158
2159
+ let isInDisconnectedSubtree = false;
2160
+
2161
function recordMount(
2162
fiber: Fiber,
2163
parentInstance: DevToolsInstance | null,
2175
}
2176
idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
2177
2176
- const id = fiberInstance.id;
2177
-
2178
if (__DEBUG__) {
2179
debug('recordMount()', fiberInstance, parentInstance);
2180
}
2181
2182
+ recordReconnect(fiberInstance, parentInstance);
2183
+ return fiberInstance;
2184
+ }
2185
+
2186
+ function recordReconnect(
2187
+ fiberInstance: FiberInstance,
2188
+ parentInstance: DevToolsInstance | null,
2189
+ ): void {
2190
+ if (isInDisconnectedSubtree) {
2191
+ // We're disconnected. We'll reconnect a hidden mount after the parent reappears.
2192
+ return;
2193
+ }
2194
+ const id = fiberInstance.id;
2195
+ const fiber = fiberInstance.data;
2196
+
2197
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
2198
2199
+ const isRoot = fiber.tag === HostRoot;
2200
+
2201
if (isRoot) {
2202
const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
2203
2309
if (isProfilingSupported) {
2310
recordProfilingDurations(fiberInstance, null);
2311
}
2295
- return fiberInstance;
2312
}
2313
2314
function recordVirtualMount(
2320
2321
idToDevToolsInstanceMap.set(id, instance);
2322
2323
+ recordVirtualReconnect(instance, parentInstance, secondaryEnv);
2324
+ }
2325
+
2326
+ function recordVirtualReconnect(
2327
+ instance: VirtualInstance,
2328
+ parentInstance: DevToolsInstance | null,
2329
+ secondaryEnv: null | string,
2330
+ ): void {
2331
+ if (isInDisconnectedSubtree) {
2332
+ // We're disconnected. We'll reconnect a hidden mount after the parent reappears.
2333
+ return;
2334
+ }
2335
const componentInfo = instance.data;
2336
2337
const key =
2383
const keyString = key === null ? null : String(key);
2384
const keyStringID = getStringID(keyString);
2385
2386
+ const id = instance.id;
2387
+
2388
pushOperation(TREE_OPERATION_ADD);
2389
pushOperation(id);
2390
pushOperation(elementType);
2399
}
2400
2401
function recordUnmount(fiberInstance: FiberInstance): void {
2372
- const fiber = fiberInstance.data;
2402
if (__DEBUG__) {
2403
debug('recordUnmount()', fiberInstance, reconcilingParent);
2404
}
2405
2406
+ recordDisconnect(fiberInstance);
2407
+
2408
+ idToDevToolsInstanceMap.delete(fiberInstance.id);
2409
+
2410
+ untrackFiber(fiberInstance, fiberInstance.data);
2411
+ }
2412
+
2413
+ function recordDisconnect(fiberInstance: FiberInstance): void {
2414
+ if (isInDisconnectedSubtree) {
2415
+ // Already disconnected.
2416
+ return;
2417
+ }
2418
+ const fiber = fiberInstance.data;
2419
+
2420
if (trackedPathMatchInstance === fiberInstance) {
2421
// We're in the process of trying to restore previous selection.
2379
- // If this fiber matched but is being unmounted, there's no use trying.
2422
+ // If this fiber matched but is being hidden, there's no use trying.
2423
// Reset the state so we don't keep holding onto it.
2424
setTrackedPath(null);
2425
}
2436
// and later arrange them in the correct order.
2437
pendingRealUnmountedIDs.push(id);
2438
}
2396
-
2397
- idToDevToolsInstanceMap.delete(fiberInstance.id);
2398
-
2399
- untrackFiber(fiberInstance, fiber);
2439
}
2440
2441
// Running state of the remaining children from the previous version of this parent that
2455
// the current parent here as well.
2456
let reconcilingParentSuspenseNode: null | SuspenseNode = null;
2457
2419
- function isSuspenseInFallback(suspenseNode: SuspenseNode) {
2420
- const fiber = suspenseNode.instance.data;
2421
- return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2422
- }
2423
-
2458
function ioExistsInSuspenseAncestor(
2459
suspenseNode: SuspenseNode,
2460
ioInfo: ReactIOInfo,
2470
}
2471
2472
function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void {
2439
- let parentSuspenseNode = reconcilingParentSuspenseNode;
2440
- while (
2441
- parentSuspenseNode !== null &&
2442
- isSuspenseInFallback(parentSuspenseNode)
2443
- ) {
2444
- // If we have something that suspends inside the fallback tree of a Suspense boundary, then
2445
- // we bubble that up to the nearest parent Suspense boundary that isn't in fallback mode.
2446
- parentSuspenseNode = parentSuspenseNode.parent;
2447
- }
2448
- if (reconcilingParent === null || parentSuspenseNode === null) {
2473
+ if (reconcilingParent === null || reconcilingParentSuspenseNode === null) {
2474
throw new Error(
2475
'It should not be possible to have suspended data outside the root. ' +
2476
'Even suspending at the first position is still a child of the root.',
2477
);
2478
}
2479
+ const parentSuspenseNode = reconcilingParentSuspenseNode;
2480
// Use the nearest unfiltered parent so that there's always some component that has
2481
// the entry on it even if you filter, or the root if all are filtered.
2482
let parentInstance = reconcilingParent;
2720
}
2721
2722
function unmountRemainingChildren() {
2697
- let child = remainingReconcilingChildren;
2698
- while (child !== null) {
2699
- unmountInstanceRecursively(child);
2700
- child = remainingReconcilingChildren;
2723
+ if (
2724
+ reconcilingParent !== null &&
2725
+ (reconcilingParent.kind === FIBER_INSTANCE ||
2726
+ reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
2727
+ reconcilingParent.data.tag === OffscreenComponent &&
2728
+ reconcilingParent.data.memoizedState !== null &&
2729
+ !isInDisconnectedSubtree
2730
+ ) {
2731
+ // This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
2732
+ isInDisconnectedSubtree = true;
2733
+ try {
2734
+ let child = remainingReconcilingChildren;
2735
+ while (child !== null) {
2736
+ unmountInstanceRecursively(child);
2737
+ child = remainingReconcilingChildren;
2738
+ }
2739
+ } finally {
2740
+ isInDisconnectedSubtree = false;
2741
+ }
2742
+ } else {
2743
+ let child = remainingReconcilingChildren;
2744
+ while (child !== null) {
2745
+ unmountInstanceRecursively(child);
2746
+ child = remainingReconcilingChildren;
2747
+ }
2748
}
2749
}
2750
2858
}
2859
2860
function recordVirtualUnmount(instance: VirtualInstance) {
2861
+ recordVirtualDisconnect(instance);
2862
+ idToDevToolsInstanceMap.delete(instance.id);
2863
+ }
2864
+
2865
+ function recordVirtualDisconnect(instance: VirtualInstance) {
2866
+ if (isInDisconnectedSubtree) {
2867
+ return;
2868
+ }
2869
if (trackedPathMatchInstance === instance) {
2870
// We're in the process of trying to restore previous selection.
2871
// If this fiber matched but is being unmounted, there's no use trying.
2875
2876
const id = instance.id;
2877
pendingRealUnmountedIDs.push(id);
2823
-
2824
- idToDevToolsInstanceMap.delete(instance.id);
2878
}
2879
2880
function getSecondaryEnvironmentName(
3083
previouslyReconciledSibling = null;
3084
remainingReconcilingChildren = null;
3085
}
3086
+ let shouldPopSuspenseNode = false;
3087
if (newSuspenseNode !== null) {
3088
reconcilingParentSuspenseNode = newSuspenseNode;
3089
previouslyReconciledSiblingSuspenseNode = null;
3090
remainingReconcilingChildrenSuspenseNodes = null;
3091
+ shouldPopSuspenseNode = true;
3092
}
3093
try {
3094
if (traceUpdatesEnabled) {
3124
}
3125
3126
if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
3072
- // If an Offscreen component is hidden, don't mount its children yet.
3127
+ // If an Offscreen component is hidden, mount its children as disconnected.
3128
+ const stashedDisconnected = isInDisconnectedSubtree;
3129
+ isInDisconnectedSubtree = true;
3130
+ try {
3131
+ if (fiber.child !== null) {
3132
+ mountChildrenRecursively(fiber.child, false);
3133
+ }
3134
+ } finally {
3135
+ isInDisconnectedSubtree = stashedDisconnected;
3136
+ }
3137
} else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) {
3138
// Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the
3139
// Offscreen wrapper itself specially.
3166
);
3167
}
3168
}
3169
+ } else if (
3170
+ fiber.tag === SuspenseComponent &&
3171
+ OffscreenComponent !== -1 &&
3172
+ newInstance !== null &&
3173
+ newSuspenseNode !== null
3174
+ ) {
3175
+ // Modern Suspense path
3176
+ const contentFiber = fiber.child;
3177
+ if (contentFiber === null) {
3178
+ throw new Error(
3179
+ 'There should always be an Offscreen Fiber child in a Suspense boundary.',
3180
+ );
3181
+ }
3182
+ const fallbackFiber = contentFiber.sibling;
3183
+
3184
+ // First update only the Offscreen boundary. I.e. the main content.
3185
+ mountVirtualChildrenRecursively(
3186
+ contentFiber,
3187
+ fallbackFiber,
3188
+ traceNearestHostComponentUpdate,
3189
+ 0, // first level
3190
+ );
3191
+
3192
+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3193
+ // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3194
+ // Since the fallback conceptually blocks the parent.
3195
+ reconcilingParentSuspenseNode = stashedSuspenseParent;
3196
+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3197
+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3198
+ shouldPopSuspenseNode = false;
3199
+ if (fallbackFiber !== null) {
3200
+ mountVirtualChildrenRecursively(
3201
+ fallbackFiber,
3202
+ null,
3203
+ traceNearestHostComponentUpdate,
3204
+ 0, // first level
3205
+ );
3206
+ }
3207
} else {
3208
if (fiber.child !== null) {
3209
mountChildrenRecursively(
3218
previouslyReconciledSibling = stashedPrevious;
3219
remainingReconcilingChildren = stashedRemaining;
3220
}
3119
- if (newSuspenseNode !== null) {
3221
+ if (shouldPopSuspenseNode) {
3222
reconcilingParentSuspenseNode = stashedSuspenseParent;
3223
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3224
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3407
let child: null | DevToolsInstance = parentInstance.firstChild;
3408
while (child !== null) {
3409
if (child.kind === FILTERED_FIBER_INSTANCE) {
3308
- addUnfilteredChildrenIDs(child, nextChildren);
3410
+ const fiber = child.data;
3411
+ if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
3412
+ // The children of this Offscreen are hidden so they don't get added.
3413
+ } else {
3414
+ addUnfilteredChildrenIDs(child, nextChildren);
3415
+ }
3416
} else {
3417
nextChildren.push(child.id);
3418
}
3849
const stashedSuspenseParent = reconcilingParentSuspenseNode;
3850
const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
3851
const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
3852
+ let shouldPopSuspenseNode = false;
3853
let previousSuspendedBy = null;
3854
if (fiberInstance !== null) {
3855
previousSuspendedBy = fiberInstance.suspendedBy;
3879
previouslyReconciledSiblingSuspenseNode = null;
3880
remainingReconcilingChildrenSuspenseNodes = suspenseNode.firstChild;
3881
suspenseNode.firstChild = null;
3882
+ shouldPopSuspenseNode = true;
3883
}
3884
}
3885
try {
3997
);
3998
shouldResetChildren = true;
3999
}
3891
- } else if (prevWasHidden && nextIsHidden) {
3892
- // We don't update any children while they're still hidden.
4000
+ } else if (nextIsHidden) {
4001
+ if (!prevWasHidden) {
4002
+ // We're hiding the children. Disconnect them from the front end but keep state.
4003
+ if (fiberInstance !== null && !isInDisconnectedSubtree) {
4004
+ disconnectChildrenRecursively(remainingReconcilingChildren);
4005
+ }
4006
+ }
4007
+ // Update children inside the hidden tree if they committed with a new updates.
4008
+ const stashedDisconnected = isInDisconnectedSubtree;
4009
+ isInDisconnectedSubtree = true;
4010
+ try {
4011
+ updateChildrenRecursively(nextFiber.child, prevFiber.child, false);
4012
+ } finally {
4013
+ isInDisconnectedSubtree = stashedDisconnected;
4014
+ }
4015
} else if (prevWasHidden && !nextIsHidden) {
4016
// We're revealing the hidden children. We now need to update them to the latest state.
3895
- if (nextFiber.child !== null) {
3896
- mountChildrenRecursively(
3897
- nextFiber.child,
3898
- traceNearestHostComponentUpdate,
4017
+ // We do this while still in the disconnected state and then we reconnect the new ones.
4018
+ // This avoids reconnecting things that are about to be removed anyway.
4019
+ const stashedDisconnected = isInDisconnectedSubtree;
4020
+ isInDisconnectedSubtree = true;
4021
+ try {
4022
+ if (nextFiber.child !== null) {
4023
+ updateChildrenRecursively(nextFiber.child, prevFiber.child, false);
4024
+ }
4025
+ // Ensure we unmount any remaining children inside the isInDisconnectedSubtree flag
4026
+ // since they should not trigger real deletions.
4027
+ unmountRemainingChildren();
4028
+ remainingReconcilingChildren = null;
4029
+ } finally {
4030
+ isInDisconnectedSubtree = stashedDisconnected;
4031
+ }
4032
+ if (fiberInstance !== null && !isInDisconnectedSubtree) {
4033
+ reconnectChildrenRecursively(fiberInstance);
4034
+ // Children may have reordered while they were hidden.
4035
+ shouldResetChildren = true;
4036
+ }
4037
+ } else if (
4038
+ nextFiber.tag === SuspenseComponent &&
4039
+ OffscreenComponent !== -1 &&
4040
+ fiberInstance !== null &&
4041
+ fiberInstance.suspenseNode !== null
4042
+ ) {
4043
+ // Modern Suspense path
4044
+ const prevContentFiber = prevFiber.child;
4045
+ const nextContentFiber = nextFiber.child;
4046
+ if (nextContentFiber === null || prevContentFiber === null) {
4047
+ throw new Error(
4048
+ 'There should always be an Offscreen Fiber child in a Suspense boundary.',
4049
);
4050
+ }
4051
+ const prevFallbackFiber = prevContentFiber.sibling;
4052
+ const nextFallbackFiber = nextContentFiber.sibling;
4053
+
4054
+ // First update only the Offscreen boundary. I.e. the main content.
4055
+ if (
4056
+ updateVirtualChildrenRecursively(
4057
+ nextContentFiber,
4058
+ nextFallbackFiber,
4059
+ prevContentFiber,
4060
+ traceNearestHostComponentUpdate,
4061
+ 0,
4062
+ )
4063
+ ) {
4064
shouldResetChildren = true;
4065
}
3902
- } else if (!prevWasHidden && nextIsHidden) {
3903
- // We're hiding the children. We really just unmount them for now.
3904
- updateChildrenRecursively(
3905
- null,
3906
- prevFiber.child,
3907
- traceNearestHostComponentUpdate,
3908
- );
3909
- shouldResetChildren = true;
4066
+
4067
+ // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4068
+ // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
4069
+ // Since the fallback conceptually blocks the parent.
4070
+ reconcilingParentSuspenseNode = stashedSuspenseParent;
4071
+ previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4072
+ remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4073
+ shouldPopSuspenseNode = false;
4074
+ if (nextFallbackFiber !== null) {
4075
+ if (
4076
+ updateVirtualChildrenRecursively(
4077
+ nextFallbackFiber,
4078
+ null,
4079
+ prevFallbackFiber,
4080
+ traceNearestHostComponentUpdate,
4081
+ 0,
4082
+ )
4083
+ ) {
4084
+ shouldResetChildren = true;
4085
+ }
4086
+ }
4087
} else {
4088
// Common case: Primary -> Primary.
4089
// This is the same code path as for non-Suspense fibers.
4177
reconcilingParent = stashedParent;
4178
previouslyReconciledSibling = stashedPrevious;
4179
remainingReconcilingChildren = stashedRemaining;
4003
- if (fiberInstance.suspenseNode !== null) {
4180
+ if (shouldPopSuspenseNode) {
4181
reconcilingParentSuspenseNode = stashedSuspenseParent;
4182
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4183
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4186
}
4187
}
4188
4189
+ function disconnectChildrenRecursively(firstChild: null | DevToolsInstance) {
4190
+ for (let child = firstChild; child !== null; child = child.nextSibling) {
4191
+ if (
4192
+ (child.kind === FIBER_INSTANCE ||
4193
+ child.kind === FILTERED_FIBER_INSTANCE) &&
4194
+ child.data.tag === OffscreenComponent &&
4195
+ child.data.memoizedState !== null
4196
+ ) {
4197
+ // This instance's children are already disconnected.
4198
+ } else {
4199
+ disconnectChildrenRecursively(child.firstChild);
4200
+ }
4201
+ if (child.kind === FIBER_INSTANCE) {
4202
+ recordDisconnect(child);
4203
+ } else if (child.kind === VIRTUAL_INSTANCE) {
4204
+ recordVirtualDisconnect(child);
4205
+ }
4206
+ }
4207
+ }
4208
+
4209
+ function reconnectChildrenRecursively(parentInstance: DevToolsInstance) {
4210
+ for (
4211
+ let child = parentInstance.firstChild;
4212
+ child !== null;
4213
+ child = child.nextSibling
4214
+ ) {
4215
+ if (child.kind === FIBER_INSTANCE) {
4216
+ recordReconnect(child, parentInstance);
4217
+ } else if (child.kind === VIRTUAL_INSTANCE) {
4218
+ const secondaryEnv = null; // TODO: We don't have this data anywhere. We could just stash it somewhere.
4219
+ recordVirtualReconnect(child, parentInstance, secondaryEnv);
4220
+ }
4221
+ if (
4222
+ (child.kind === FIBER_INSTANCE ||
4223
+ child.kind === FILTERED_FIBER_INSTANCE) &&
4224
+ child.data.tag === OffscreenComponent &&
4225
+ child.data.memoizedState !== null
4226
+ ) {
4227
+ // This instance's children should remain disconnected.
4228
+ } else {
4229
+ reconnectChildrenRecursively(child);
4230
+ }
4231
+ }
4232
+ }
4233
+
4234
function cleanup() {
4235
isProfiling = false;
4236
}