Skip to content

Commit

Permalink
Land forked reconciler changes (#24878)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
acdlite committed Jul 8, 2022
1 parent 5e4e2da commit 30eb267
Show file tree
Hide file tree
Showing 11 changed files with 634 additions and 386 deletions.
359 changes: 223 additions & 136 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js

Large diffs are not rendered by default.

105 changes: 75 additions & 30 deletions packages/react-reconciler/src/ReactFiberCommitWork.old.js
Expand Up @@ -1962,40 +1962,65 @@ function commitSuspenseHydrationCallbacks(
}
}

function attachSuspenseRetryListeners(finishedWork: Fiber) {
function getRetryCache(finishedWork) {
// TODO: Unify the interface for the retry cache so we don't have to switch
// on the tag like this.
switch (finishedWork.tag) {
case SuspenseComponent:
case SuspenseListComponent: {
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
return retryCache;
}
case OffscreenComponent: {
const instance: OffscreenInstance = finishedWork.stateNode;
let retryCache = instance.retryCache;
if (retryCache === null) {
retryCache = instance.retryCache = new PossiblyWeakSet();
}
return retryCache;
}
default: {
throw new Error(
`Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` +
'bug in React.',
);
}
}
}

function attachSuspenseRetryListeners(
finishedWork: Fiber,
wakeables: Set<Wakeable>,
) {
// If this boundary just timed out, then it will have a set of wakeables.
// For each wakeable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
retryCache.add(wakeable);

if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
const retryCache = getRetryCache(finishedWork);
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
retryCache.add(wakeable);

if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
}

wakeable.then(retry, retry);
}
});
}

wakeable.then(retry, retry);
}
});
}

// This function detects when a Suspense boundary goes from visible to hidden.
Expand Down Expand Up @@ -2325,7 +2350,11 @@ function commitMutationEffectsOnFiber(
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
attachSuspenseRetryListeners(finishedWork);
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
attachSuspenseRetryListeners(finishedWork, wakeables);
}
}
return;
}
Expand Down Expand Up @@ -2383,14 +2412,30 @@ function commitMutationEffectsOnFiber(
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
}
}

// TODO: Move to passive phase
if (flags & Update) {
const offscreenQueue: OffscreenQueue | null = (finishedWork.updateQueue: any);
if (offscreenQueue !== null) {
const wakeables = offscreenQueue.wakeables;
if (wakeables !== null) {
offscreenQueue.wakeables = null;
attachSuspenseRetryListeners(finishedWork, wakeables);
}
}
}
return;
}
case SuspenseListComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

if (flags & Update) {
attachSuspenseRetryListeners(finishedWork);
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
attachSuspenseRetryListeners(finishedWork, wakeables);
}
}
return;
}
Expand Down
87 changes: 49 additions & 38 deletions packages/react-reconciler/src/ReactFiberCompleteWork.old.js
Expand Up @@ -27,7 +27,6 @@ import type {
SuspenseState,
SuspenseListRenderState,
} from './ReactFiberSuspenseComponent.old';
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
import type {Cache} from './ReactFiberCacheComponent.old';
Expand Down Expand Up @@ -110,14 +109,17 @@ import {
} from './ReactFiberHostContext.old';
import {
suspenseStackCursor,
InvisibleParentSuspenseContext,
hasSuspenseContext,
popSuspenseContext,
pushSuspenseContext,
setShallowSuspenseContext,
popSuspenseListContext,
popSuspenseHandler,
pushSuspenseListContext,
setShallowSuspenseListContext,
ForceSuspenseFallback,
setDefaultShallowSuspenseContext,
setDefaultShallowSuspenseListContext,
} from './ReactFiberSuspenseContext.old';
import {
popHiddenContext,
isCurrentTreeHidden,
} from './ReactFiberHiddenContext.old';
import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
import {
isContextProvider as isLegacyContextProvider,
Expand Down Expand Up @@ -147,9 +149,7 @@ import {
renderDidSuspend,
renderDidSuspendDelayIfPossible,
renderHasNotSuspendedYet,
popRenderLanes,
getRenderTargetTime,
subtreeRenderLanes,
getWorkInProgressTransitions,
} from './ReactFiberWorkLoop.old';
import {
Expand Down Expand Up @@ -1086,7 +1086,7 @@ function completeWork(
return null;
}
case SuspenseComponent: {
popSuspenseContext(workInProgress);
popSuspenseHandler(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;

// Special path for dehydrated boundaries. We may eventually move this
Expand Down Expand Up @@ -1195,25 +1195,23 @@ function completeWork(
// If this render already had a ping or lower pri updates,
// and this is the first time we know we're going to suspend we
// should be able to immediately restart from within throwException.
const hasInvisibleChildContext =
current === null &&
(workInProgress.memoizedProps.unstable_avoidThisFallback !==
true ||
!enableSuspenseAvoidThisFallback);
if (
hasInvisibleChildContext ||
hasSuspenseContext(
suspenseStackCursor.current,
(InvisibleParentSuspenseContext: SuspenseContext),
)
) {
// If this was in an invisible tree or a new render, then showing
// this boundary is ok.
renderDidSuspend();
} else {
// Otherwise, we're going to have to hide content so we should
// suspend for longer if possible.

// Check if this is a "bad" fallback state or a good one. A bad
// fallback state is one that we only show as a last resort; if this
// is a transition, we'll block it from displaying, and wait for
// more data to arrive.
const isBadFallback =
// It's bad to switch to a fallback if content is already visible
(current !== null && !prevDidTimeout && !isCurrentTreeHidden()) ||
// Experimental: Some fallbacks are always bad
(enableSuspenseAvoidThisFallback &&
workInProgress.memoizedProps.unstable_avoidThisFallback ===
true);

if (isBadFallback) {
renderDidSuspendDelayIfPossible();
} else {
renderDidSuspend();
}
}
}
Expand Down Expand Up @@ -1275,7 +1273,7 @@ function completeWork(
return null;
}
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
popSuspenseListContext(workInProgress);

const renderState: null | SuspenseListRenderState =
workInProgress.memoizedState;
Expand Down Expand Up @@ -1341,11 +1339,11 @@ function completeWork(
workInProgress.subtreeFlags = NoFlags;
resetChildFibers(workInProgress, renderLanes);

// Set up the Suspense Context to force suspense and immediately
// rerender the children.
pushSuspenseContext(
// Set up the Suspense List Context to force suspense and
// immediately rerender the children.
pushSuspenseListContext(
workInProgress,
setShallowSuspenseContext(
setShallowSuspenseListContext(
suspenseStackCursor.current,
ForceSuspenseFallback,
),
Expand Down Expand Up @@ -1468,14 +1466,16 @@ function completeWork(
// setting it the first time we go from not suspended to suspended.
let suspenseContext = suspenseStackCursor.current;
if (didSuspendAlready) {
suspenseContext = setShallowSuspenseContext(
suspenseContext = setShallowSuspenseListContext(
suspenseContext,
ForceSuspenseFallback,
);
} else {
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
suspenseContext = setDefaultShallowSuspenseListContext(
suspenseContext,
);
}
pushSuspenseContext(workInProgress, suspenseContext);
pushSuspenseListContext(workInProgress, suspenseContext);
// Do a pass over the next row.
// Don't bubble properties in this case.
return next;
Expand Down Expand Up @@ -1508,7 +1508,8 @@ function completeWork(
}
case OffscreenComponent:
case LegacyHiddenComponent: {
popRenderLanes(workInProgress);
popSuspenseHandler(workInProgress);
popHiddenContext(workInProgress);
const nextState: OffscreenState | null = workInProgress.memoizedState;
const nextIsHidden = nextState !== null;

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

if (workInProgress.updateQueue !== null) {
// Schedule an effect to attach Suspense retry listeners
// TODO: Move to passive phase
workInProgress.flags |= Update;
}

if (enableCache) {
let previousCache: Cache | null = null;
if (
Expand Down
71 changes: 70 additions & 1 deletion packages/react-reconciler/src/ReactFiberHiddenContext.old.js
@@ -1 +1,70 @@
// Intentionally blank. File only exists in new reconciler fork.
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {Fiber} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack.old';
import type {Lanes} from './ReactFiberLane.old';

import {createCursor, push, pop} from './ReactFiberStack.old';

import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop.old';
import {NoLanes, mergeLanes} from './ReactFiberLane.old';

// TODO: Remove `renderLanes` context in favor of hidden context
type HiddenContext = {
// Represents the lanes that must be included when processing updates in
// order to reveal the hidden content.
// TODO: Remove `subtreeLanes` context from work loop in favor of this one.
baseLanes: number,
};

// TODO: This isn't being used yet, but it's intended to replace the
// InvisibleParentContext that is currently managed by SuspenseContext.
export const currentTreeHiddenStackCursor: StackCursor<HiddenContext | null> = createCursor(
null,
);
export const prevRenderLanesStackCursor: StackCursor<Lanes> = createCursor(
NoLanes,
);

export function pushHiddenContext(fiber: Fiber, context: HiddenContext): void {
const prevRenderLanes = getRenderLanes();
push(prevRenderLanesStackCursor, prevRenderLanes, fiber);
push(currentTreeHiddenStackCursor, context, fiber);

// When rendering a subtree that's currently hidden, we must include all
// lanes that would have rendered if the hidden subtree hadn't been deferred.
// That is, in order to reveal content from hidden -> visible, we must commit
// all the updates that we skipped when we originally hid the tree.
setRenderLanes(mergeLanes(prevRenderLanes, context.baseLanes));
}

export function reuseHiddenContextOnStack(fiber: Fiber): void {
// This subtree is not currently hidden, so we don't need to add any lanes
// to the render lanes. But we still need to push something to avoid a
// context mismatch. Reuse the existing context on the stack.
push(prevRenderLanesStackCursor, getRenderLanes(), fiber);
push(
currentTreeHiddenStackCursor,
currentTreeHiddenStackCursor.current,
fiber,
);
}

export function popHiddenContext(fiber: Fiber): void {
// Restore the previous render lanes from the stack
setRenderLanes(prevRenderLanesStackCursor.current);

pop(currentTreeHiddenStackCursor, fiber);
pop(prevRenderLanesStackCursor, fiber);
}

export function isCurrentTreeHidden() {
return currentTreeHiddenStackCursor.current !== null;
}

0 comments on commit 30eb267

Please sign in to comment.