Skip to content

Commit 30eb267

Browse files
authored
Land forked reconciler changes (#24878)
This applies forked changes from the "new" reconciler to the "old" one. Includes: - 67de5e3 [FORKED] Hidden trees should capture Suspense - 6ab05ee [FORKED] Track nearest Suspense handler on stack - 051ac55 [FORKED] Add HiddenContext to track if subtree is hidden
1 parent 5e4e2da commit 30eb267

11 files changed

+634
-386
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.old.js

+223-136
Large diffs are not rendered by default.

packages/react-reconciler/src/ReactFiberCommitWork.old.js

+75-30
Original file line numberDiff line numberDiff line change
@@ -1962,40 +1962,65 @@ function commitSuspenseHydrationCallbacks(
19621962
}
19631963
}
19641964

1965-
function attachSuspenseRetryListeners(finishedWork: Fiber) {
1965+
function getRetryCache(finishedWork) {
1966+
// TODO: Unify the interface for the retry cache so we don't have to switch
1967+
// on the tag like this.
1968+
switch (finishedWork.tag) {
1969+
case SuspenseComponent:
1970+
case SuspenseListComponent: {
1971+
let retryCache = finishedWork.stateNode;
1972+
if (retryCache === null) {
1973+
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1974+
}
1975+
return retryCache;
1976+
}
1977+
case OffscreenComponent: {
1978+
const instance: OffscreenInstance = finishedWork.stateNode;
1979+
let retryCache = instance.retryCache;
1980+
if (retryCache === null) {
1981+
retryCache = instance.retryCache = new PossiblyWeakSet();
1982+
}
1983+
return retryCache;
1984+
}
1985+
default: {
1986+
throw new Error(
1987+
`Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` +
1988+
'bug in React.',
1989+
);
1990+
}
1991+
}
1992+
}
1993+
1994+
function attachSuspenseRetryListeners(
1995+
finishedWork: Fiber,
1996+
wakeables: Set<Wakeable>,
1997+
) {
19661998
// If this boundary just timed out, then it will have a set of wakeables.
19671999
// For each wakeable, attach a listener so that when it resolves, React
19682000
// attempts to re-render the boundary in the primary (pre-timeout) state.
1969-
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
1970-
if (wakeables !== null) {
1971-
finishedWork.updateQueue = null;
1972-
let retryCache = finishedWork.stateNode;
1973-
if (retryCache === null) {
1974-
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1975-
}
1976-
wakeables.forEach(wakeable => {
1977-
// Memoize using the boundary fiber to prevent redundant listeners.
1978-
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
1979-
if (!retryCache.has(wakeable)) {
1980-
retryCache.add(wakeable);
1981-
1982-
if (enableUpdaterTracking) {
1983-
if (isDevToolsPresent) {
1984-
if (inProgressLanes !== null && inProgressRoot !== null) {
1985-
// If we have pending work still, associate the original updaters with it.
1986-
restorePendingUpdaters(inProgressRoot, inProgressLanes);
1987-
} else {
1988-
throw Error(
1989-
'Expected finished root and lanes to be set. This is a bug in React.',
1990-
);
1991-
}
2001+
const retryCache = getRetryCache(finishedWork);
2002+
wakeables.forEach(wakeable => {
2003+
// Memoize using the boundary fiber to prevent redundant listeners.
2004+
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
2005+
if (!retryCache.has(wakeable)) {
2006+
retryCache.add(wakeable);
2007+
2008+
if (enableUpdaterTracking) {
2009+
if (isDevToolsPresent) {
2010+
if (inProgressLanes !== null && inProgressRoot !== null) {
2011+
// If we have pending work still, associate the original updaters with it.
2012+
restorePendingUpdaters(inProgressRoot, inProgressLanes);
2013+
} else {
2014+
throw Error(
2015+
'Expected finished root and lanes to be set. This is a bug in React.',
2016+
);
19922017
}
19932018
}
1994-
1995-
wakeable.then(retry, retry);
19962019
}
1997-
});
1998-
}
2020+
2021+
wakeable.then(retry, retry);
2022+
}
2023+
});
19992024
}
20002025

20012026
// This function detects when a Suspense boundary goes from visible to hidden.
@@ -2325,7 +2350,11 @@ function commitMutationEffectsOnFiber(
23252350
} catch (error) {
23262351
captureCommitPhaseError(finishedWork, finishedWork.return, error);
23272352
}
2328-
attachSuspenseRetryListeners(finishedWork);
2353+
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
2354+
if (wakeables !== null) {
2355+
finishedWork.updateQueue = null;
2356+
attachSuspenseRetryListeners(finishedWork, wakeables);
2357+
}
23292358
}
23302359
return;
23312360
}
@@ -2383,14 +2412,30 @@ function commitMutationEffectsOnFiber(
23832412
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
23842413
}
23852414
}
2415+
2416+
// TODO: Move to passive phase
2417+
if (flags & Update) {
2418+
const offscreenQueue: OffscreenQueue | null = (finishedWork.updateQueue: any);
2419+
if (offscreenQueue !== null) {
2420+
const wakeables = offscreenQueue.wakeables;
2421+
if (wakeables !== null) {
2422+
offscreenQueue.wakeables = null;
2423+
attachSuspenseRetryListeners(finishedWork, wakeables);
2424+
}
2425+
}
2426+
}
23862427
return;
23872428
}
23882429
case SuspenseListComponent: {
23892430
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
23902431
commitReconciliationEffects(finishedWork);
23912432

23922433
if (flags & Update) {
2393-
attachSuspenseRetryListeners(finishedWork);
2434+
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
2435+
if (wakeables !== null) {
2436+
finishedWork.updateQueue = null;
2437+
attachSuspenseRetryListeners(finishedWork, wakeables);
2438+
}
23942439
}
23952440
return;
23962441
}

packages/react-reconciler/src/ReactFiberCompleteWork.old.js

+49-38
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import type {
2727
SuspenseState,
2828
SuspenseListRenderState,
2929
} from './ReactFiberSuspenseComponent.old';
30-
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
3130
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3231
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
3332
import type {Cache} from './ReactFiberCacheComponent.old';
@@ -110,14 +109,17 @@ import {
110109
} from './ReactFiberHostContext.old';
111110
import {
112111
suspenseStackCursor,
113-
InvisibleParentSuspenseContext,
114-
hasSuspenseContext,
115-
popSuspenseContext,
116-
pushSuspenseContext,
117-
setShallowSuspenseContext,
112+
popSuspenseListContext,
113+
popSuspenseHandler,
114+
pushSuspenseListContext,
115+
setShallowSuspenseListContext,
118116
ForceSuspenseFallback,
119-
setDefaultShallowSuspenseContext,
117+
setDefaultShallowSuspenseListContext,
120118
} from './ReactFiberSuspenseContext.old';
119+
import {
120+
popHiddenContext,
121+
isCurrentTreeHidden,
122+
} from './ReactFiberHiddenContext.old';
121123
import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
122124
import {
123125
isContextProvider as isLegacyContextProvider,
@@ -147,9 +149,7 @@ import {
147149
renderDidSuspend,
148150
renderDidSuspendDelayIfPossible,
149151
renderHasNotSuspendedYet,
150-
popRenderLanes,
151152
getRenderTargetTime,
152-
subtreeRenderLanes,
153153
getWorkInProgressTransitions,
154154
} from './ReactFiberWorkLoop.old';
155155
import {
@@ -1086,7 +1086,7 @@ function completeWork(
10861086
return null;
10871087
}
10881088
case SuspenseComponent: {
1089-
popSuspenseContext(workInProgress);
1089+
popSuspenseHandler(workInProgress);
10901090
const nextState: null | SuspenseState = workInProgress.memoizedState;
10911091

10921092
// Special path for dehydrated boundaries. We may eventually move this
@@ -1195,25 +1195,23 @@ function completeWork(
11951195
// If this render already had a ping or lower pri updates,
11961196
// and this is the first time we know we're going to suspend we
11971197
// should be able to immediately restart from within throwException.
1198-
const hasInvisibleChildContext =
1199-
current === null &&
1200-
(workInProgress.memoizedProps.unstable_avoidThisFallback !==
1201-
true ||
1202-
!enableSuspenseAvoidThisFallback);
1203-
if (
1204-
hasInvisibleChildContext ||
1205-
hasSuspenseContext(
1206-
suspenseStackCursor.current,
1207-
(InvisibleParentSuspenseContext: SuspenseContext),
1208-
)
1209-
) {
1210-
// If this was in an invisible tree or a new render, then showing
1211-
// this boundary is ok.
1212-
renderDidSuspend();
1213-
} else {
1214-
// Otherwise, we're going to have to hide content so we should
1215-
// suspend for longer if possible.
1198+
1199+
// Check if this is a "bad" fallback state or a good one. A bad
1200+
// fallback state is one that we only show as a last resort; if this
1201+
// is a transition, we'll block it from displaying, and wait for
1202+
// more data to arrive.
1203+
const isBadFallback =
1204+
// It's bad to switch to a fallback if content is already visible
1205+
(current !== null && !prevDidTimeout && !isCurrentTreeHidden()) ||
1206+
// Experimental: Some fallbacks are always bad
1207+
(enableSuspenseAvoidThisFallback &&
1208+
workInProgress.memoizedProps.unstable_avoidThisFallback ===
1209+
true);
1210+
1211+
if (isBadFallback) {
12161212
renderDidSuspendDelayIfPossible();
1213+
} else {
1214+
renderDidSuspend();
12171215
}
12181216
}
12191217
}
@@ -1275,7 +1273,7 @@ function completeWork(
12751273
return null;
12761274
}
12771275
case SuspenseListComponent: {
1278-
popSuspenseContext(workInProgress);
1276+
popSuspenseListContext(workInProgress);
12791277

12801278
const renderState: null | SuspenseListRenderState =
12811279
workInProgress.memoizedState;
@@ -1341,11 +1339,11 @@ function completeWork(
13411339
workInProgress.subtreeFlags = NoFlags;
13421340
resetChildFibers(workInProgress, renderLanes);
13431341

1344-
// Set up the Suspense Context to force suspense and immediately
1345-
// rerender the children.
1346-
pushSuspenseContext(
1342+
// Set up the Suspense List Context to force suspense and
1343+
// immediately rerender the children.
1344+
pushSuspenseListContext(
13471345
workInProgress,
1348-
setShallowSuspenseContext(
1346+
setShallowSuspenseListContext(
13491347
suspenseStackCursor.current,
13501348
ForceSuspenseFallback,
13511349
),
@@ -1468,14 +1466,16 @@ function completeWork(
14681466
// setting it the first time we go from not suspended to suspended.
14691467
let suspenseContext = suspenseStackCursor.current;
14701468
if (didSuspendAlready) {
1471-
suspenseContext = setShallowSuspenseContext(
1469+
suspenseContext = setShallowSuspenseListContext(
14721470
suspenseContext,
14731471
ForceSuspenseFallback,
14741472
);
14751473
} else {
1476-
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
1474+
suspenseContext = setDefaultShallowSuspenseListContext(
1475+
suspenseContext,
1476+
);
14771477
}
1478-
pushSuspenseContext(workInProgress, suspenseContext);
1478+
pushSuspenseListContext(workInProgress, suspenseContext);
14791479
// Do a pass over the next row.
14801480
// Don't bubble properties in this case.
14811481
return next;
@@ -1508,7 +1508,8 @@ function completeWork(
15081508
}
15091509
case OffscreenComponent:
15101510
case LegacyHiddenComponent: {
1511-
popRenderLanes(workInProgress);
1511+
popSuspenseHandler(workInProgress);
1512+
popHiddenContext(workInProgress);
15121513
const nextState: OffscreenState | null = workInProgress.memoizedState;
15131514
const nextIsHidden = nextState !== null;
15141515

@@ -1529,7 +1530,11 @@ function completeWork(
15291530
} else {
15301531
// Don't bubble properties for hidden children unless we're rendering
15311532
// at offscreen priority.
1532-
if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
1533+
if (
1534+
includesSomeLane(renderLanes, (OffscreenLane: Lane)) &&
1535+
// Also don't bubble if the tree suspended
1536+
(workInProgress.flags & DidCapture) === NoLanes
1537+
) {
15331538
bubbleProperties(workInProgress);
15341539
// Check if there was an insertion or update in the hidden subtree.
15351540
// If so, we need to hide those nodes in the commit phase, so
@@ -1544,6 +1549,12 @@ function completeWork(
15441549
}
15451550
}
15461551

1552+
if (workInProgress.updateQueue !== null) {
1553+
// Schedule an effect to attach Suspense retry listeners
1554+
// TODO: Move to passive phase
1555+
workInProgress.flags |= Update;
1556+
}
1557+
15471558
if (enableCache) {
15481559
let previousCache: Cache | null = null;
15491560
if (
Original file line numberDiff line numberDiff line change
@@ -1 +1,70 @@
1-
// Intentionally blank. File only exists in new reconciler fork.
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Fiber} from './ReactInternalTypes';
11+
import type {StackCursor} from './ReactFiberStack.old';
12+
import type {Lanes} from './ReactFiberLane.old';
13+
14+
import {createCursor, push, pop} from './ReactFiberStack.old';
15+
16+
import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop.old';
17+
import {NoLanes, mergeLanes} from './ReactFiberLane.old';
18+
19+
// TODO: Remove `renderLanes` context in favor of hidden context
20+
type HiddenContext = {
21+
// Represents the lanes that must be included when processing updates in
22+
// order to reveal the hidden content.
23+
// TODO: Remove `subtreeLanes` context from work loop in favor of this one.
24+
baseLanes: number,
25+
};
26+
27+
// TODO: This isn't being used yet, but it's intended to replace the
28+
// InvisibleParentContext that is currently managed by SuspenseContext.
29+
export const currentTreeHiddenStackCursor: StackCursor<HiddenContext | null> = createCursor(
30+
null,
31+
);
32+
export const prevRenderLanesStackCursor: StackCursor<Lanes> = createCursor(
33+
NoLanes,
34+
);
35+
36+
export function pushHiddenContext(fiber: Fiber, context: HiddenContext): void {
37+
const prevRenderLanes = getRenderLanes();
38+
push(prevRenderLanesStackCursor, prevRenderLanes, fiber);
39+
push(currentTreeHiddenStackCursor, context, fiber);
40+
41+
// When rendering a subtree that's currently hidden, we must include all
42+
// lanes that would have rendered if the hidden subtree hadn't been deferred.
43+
// That is, in order to reveal content from hidden -> visible, we must commit
44+
// all the updates that we skipped when we originally hid the tree.
45+
setRenderLanes(mergeLanes(prevRenderLanes, context.baseLanes));
46+
}
47+
48+
export function reuseHiddenContextOnStack(fiber: Fiber): void {
49+
// This subtree is not currently hidden, so we don't need to add any lanes
50+
// to the render lanes. But we still need to push something to avoid a
51+
// context mismatch. Reuse the existing context on the stack.
52+
push(prevRenderLanesStackCursor, getRenderLanes(), fiber);
53+
push(
54+
currentTreeHiddenStackCursor,
55+
currentTreeHiddenStackCursor.current,
56+
fiber,
57+
);
58+
}
59+
60+
export function popHiddenContext(fiber: Fiber): void {
61+
// Restore the previous render lanes from the stack
62+
setRenderLanes(prevRenderLanesStackCursor.current);
63+
64+
pop(currentTreeHiddenStackCursor, fiber);
65+
pop(prevRenderLanesStackCursor, fiber);
66+
}
67+
68+
export function isCurrentTreeHidden() {
69+
return currentTreeHiddenStackCursor.current !== null;
70+
}

0 commit comments

Comments
 (0)