mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Proposed new Suspense layout effect semantics (#21079)
This commit contains a proposed change to layout effect semantics within Suspense subtrees: If a component mounts within a Suspense boundary and is later hidden (because of something else suspending) React will cleanup that component’s layout effects (including React-managed refs). This change will hopefully fix existing bugs that occur because of things like reading layout in a hidden tree and will also enable a point at which to e.g. pause videos and hide user-managed portals. After the suspended boundary resolves, React will setup the component’s layout effects again (including React-managed refs). The scenario described above is not common. The useTransition API should ensure that Suspense does not revert to its fallback state after being mounted. Note that these changes are primarily written in terms of the (as of yet internal) Offscreen API as we intend to provide similar effects semantics within recently shown/hidden Offscreen trees in the future. (More to follow.) (Note that all changes in this PR are behind a new feature flag, enableSuspenseLayoutEffectSemantics, which is disabled for now.)
This commit is contained in:
@@ -66,6 +66,7 @@ import {
|
||||
DidCapture,
|
||||
Update,
|
||||
Ref,
|
||||
RefStatic,
|
||||
ChildDeletion,
|
||||
ForceUpdateForLegacySuspense,
|
||||
StaticMask,
|
||||
@@ -83,6 +84,7 @@ import {
|
||||
enableScopeAPI,
|
||||
enableCache,
|
||||
enableLazyContextPropagation,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import invariant from 'shared/invariant';
|
||||
import shallowEqual from 'shared/shallowEqual';
|
||||
@@ -854,6 +856,9 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
|
||||
) {
|
||||
// Schedule a Ref effect
|
||||
workInProgress.flags |= Ref;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
workInProgress.flags |= RefStatic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
DidCapture,
|
||||
Update,
|
||||
Ref,
|
||||
RefStatic,
|
||||
ChildDeletion,
|
||||
ForceUpdateForLegacySuspense,
|
||||
StaticMask,
|
||||
@@ -83,6 +84,7 @@ import {
|
||||
enableScopeAPI,
|
||||
enableCache,
|
||||
enableLazyContextPropagation,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import invariant from 'shared/invariant';
|
||||
import shallowEqual from 'shared/shallowEqual';
|
||||
@@ -854,6 +856,9 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
|
||||
) {
|
||||
// Schedule a Ref effect
|
||||
workInProgress.flags |= Ref;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
workInProgress.flags |= RefStatic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
import type {Fiber} from './ReactInternalTypes';
|
||||
import type {Lanes} from './ReactFiberLane.new';
|
||||
import type {UpdateQueue} from './ReactUpdateQueue.new';
|
||||
import type {Flags} from './ReactFiberFlags';
|
||||
|
||||
import * as React from 'react';
|
||||
import {MountLayoutDev, Update, Snapshot} from './ReactFiberFlags';
|
||||
import {
|
||||
LayoutStatic,
|
||||
MountLayoutDev,
|
||||
Update,
|
||||
Snapshot,
|
||||
} from './ReactFiberFlags';
|
||||
import {
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
disableLegacyContext,
|
||||
@@ -21,6 +27,7 @@ import {
|
||||
warnAboutDeprecatedLifecycles,
|
||||
enableStrictEffects,
|
||||
enableLazyContextPropagation,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
|
||||
import {isMounted} from './ReactFiberTreeReflection';
|
||||
@@ -908,16 +915,19 @@ function mountClassInstance(
|
||||
}
|
||||
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,16 +997,19 @@ function resumeMountClassInstance(
|
||||
// If an update was already in progress, we should schedule an Update
|
||||
// effect even though we're bailing out, so that cWU/cDU are called.
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1039,31 +1052,37 @@ function resumeMountClassInstance(
|
||||
}
|
||||
}
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
} else {
|
||||
// If an update was already in progress, we should schedule an Update
|
||||
// effect even though we're bailing out, so that cWU/cDU are called.
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
|
||||
// If shouldComponentUpdate returned false, we should still update the
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
import type {Fiber} from './ReactInternalTypes';
|
||||
import type {Lanes} from './ReactFiberLane.old';
|
||||
import type {UpdateQueue} from './ReactUpdateQueue.old';
|
||||
import type {Flags} from './ReactFiberFlags';
|
||||
|
||||
import * as React from 'react';
|
||||
import {MountLayoutDev, Update, Snapshot} from './ReactFiberFlags';
|
||||
import {
|
||||
LayoutStatic,
|
||||
MountLayoutDev,
|
||||
Update,
|
||||
Snapshot,
|
||||
} from './ReactFiberFlags';
|
||||
import {
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
disableLegacyContext,
|
||||
@@ -21,6 +27,7 @@ import {
|
||||
warnAboutDeprecatedLifecycles,
|
||||
enableStrictEffects,
|
||||
enableLazyContextPropagation,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
|
||||
import {isMounted} from './ReactFiberTreeReflection';
|
||||
@@ -908,16 +915,19 @@ function mountClassInstance(
|
||||
}
|
||||
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,16 +997,19 @@ function resumeMountClassInstance(
|
||||
// If an update was already in progress, we should schedule an Update
|
||||
// effect even though we're bailing out, so that cWU/cDU are called.
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1039,31 +1052,37 @@ function resumeMountClassInstance(
|
||||
}
|
||||
}
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
} else {
|
||||
// If an update was already in progress, we should schedule an Update
|
||||
// effect even though we're bailing out, so that cWU/cDU are called.
|
||||
if (typeof instance.componentDidMount === 'function') {
|
||||
let fiberFlags: Flags = Update;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStatic;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(workInProgress.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
// Never double-invoke effects for legacy roots.
|
||||
workInProgress.flags |= MountLayoutDev | Update;
|
||||
} else {
|
||||
workInProgress.flags |= Update;
|
||||
fiberFlags |= MountLayoutDev;
|
||||
}
|
||||
workInProgress.flags |= fiberFlags;
|
||||
}
|
||||
|
||||
// If shouldComponentUpdate returned false, we should still update the
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
enableScopeAPI,
|
||||
enableStrictEffects,
|
||||
deletedTreeCleanUpLevel,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -79,6 +80,8 @@ import {
|
||||
MutationMask,
|
||||
LayoutMask,
|
||||
PassiveMask,
|
||||
LayoutStatic,
|
||||
RefStatic,
|
||||
} from './ReactFiberFlags';
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -97,7 +100,7 @@ import {
|
||||
recordPassiveEffectDuration,
|
||||
startPassiveEffectTimer,
|
||||
} from './ReactProfilerTimer.new';
|
||||
import {ProfileMode} from './ReactTypeOfMode';
|
||||
import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
|
||||
import {commitUpdateQueue} from './ReactUpdateQueue.new';
|
||||
import {
|
||||
getPublicInstance,
|
||||
@@ -149,6 +152,14 @@ if (__DEV__) {
|
||||
didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
|
||||
}
|
||||
|
||||
// Used during the commit phase to track the state of the Offscreen component stack.
|
||||
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
|
||||
// Only used when enableSuspenseLayoutEffectSemantics is enabled.
|
||||
let offscreenSubtreeIsHidden: boolean = false;
|
||||
const offscreenSubtreeIsHiddenStack: Array<boolean> = [];
|
||||
let offscreenSubtreeWasHidden: boolean = false;
|
||||
const offscreenSubtreeWasHiddenStack: Array<boolean> = [];
|
||||
|
||||
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
|
||||
|
||||
let nextEffect: Fiber | null = null;
|
||||
@@ -172,6 +183,32 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
|
||||
}
|
||||
};
|
||||
|
||||
// Capture errors so they don't interrupt mounting.
|
||||
function safelyCallCommitHookLayoutEffectListMount(
|
||||
current: Fiber,
|
||||
nearestMountedAncestor: Fiber | null,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
commitHookEffectListMount,
|
||||
null,
|
||||
HookLayout,
|
||||
current,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitHookEffectListMount(HookLayout, current);
|
||||
} catch (unmountError) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors so they don't interrupt unmounting.
|
||||
function safelyCallComponentWillUnmount(
|
||||
current: Fiber,
|
||||
@@ -199,6 +236,44 @@ function safelyCallComponentWillUnmount(
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors so they don't interrupt mounting.
|
||||
function safelyCallComponentDidMount(
|
||||
current: Fiber,
|
||||
nearestMountedAncestor: Fiber | null,
|
||||
instance: any,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(null, instance.componentDidMount, instance);
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
instance.componentDidMount();
|
||||
} catch (unmountError) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors so they don't interrupt mounting.
|
||||
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(null, commitAttachRef, null, current);
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitAttachRef(current);
|
||||
} catch (unmountError) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
|
||||
const ref = current.ref;
|
||||
if (ref !== null) {
|
||||
@@ -942,6 +1017,12 @@ function commitLayoutEffectOnFiber(
|
||||
}
|
||||
|
||||
function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
// Suspense layout effects semantics don't change for legacy roots.
|
||||
const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
const current = finishedWork.alternate;
|
||||
const wasHidden = current !== null && current.memoizedState !== null;
|
||||
|
||||
if (supportsMutation) {
|
||||
// We only have the top Fiber that was inserted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
@@ -954,6 +1035,25 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
} else {
|
||||
unhideInstance(node.stateNode, node.memoizedProps);
|
||||
}
|
||||
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
// This method is called during mutation; it should detach refs within a hidden subtree.
|
||||
// Attaching refs should be done elsewhere though (during layout).
|
||||
if ((node.flags & RefStatic) !== NoFlags) {
|
||||
if (isHidden) {
|
||||
safelyDetachRef(node, finishedWork);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(node.subtreeFlags & (RefStatic | LayoutStatic)) !== NoFlags &&
|
||||
node.child !== null
|
||||
) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (node.tag === HostText) {
|
||||
const instance = node.stateNode;
|
||||
if (isHidden) {
|
||||
@@ -967,13 +1067,61 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
(node.memoizedState: OffscreenState) !== null &&
|
||||
node !== finishedWork
|
||||
) {
|
||||
// Found a nested Offscreen component that is hidden. Don't search
|
||||
// any deeper. This tree should remain hidden.
|
||||
// Found a nested Offscreen component that is hidden.
|
||||
// Don't search any deeper. This tree should remain hidden.
|
||||
} else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
// When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
|
||||
if ((node.flags & (RefStatic | LayoutStatic)) !== NoFlags) {
|
||||
switch (node.tag) {
|
||||
case FunctionComponent:
|
||||
case ForwardRef:
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent: {
|
||||
// Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
|
||||
if (isHidden && !wasHidden) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableProfilerCommitHooks &&
|
||||
node.mode & ProfileMode
|
||||
) {
|
||||
try {
|
||||
startLayoutEffectTimer();
|
||||
commitHookEffectListUnmount(HookLayout, node, finishedWork);
|
||||
} finally {
|
||||
recordLayoutEffectDuration(node);
|
||||
}
|
||||
} else {
|
||||
commitHookEffectListUnmount(HookLayout, node, finishedWork);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClassComponent: {
|
||||
if (isHidden && !wasHidden) {
|
||||
if ((node.flags & RefStatic) !== NoFlags) {
|
||||
safelyDetachRef(node, finishedWork);
|
||||
}
|
||||
const instance = node.stateNode;
|
||||
if (typeof instance.componentWillUnmount === 'function') {
|
||||
safelyCallComponentWillUnmount(node, finishedWork, instance);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
} else if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node === finishedWork) {
|
||||
return;
|
||||
}
|
||||
@@ -2143,13 +2291,49 @@ function commitLayoutEffects_begin(
|
||||
root: FiberRoot,
|
||||
committedLanes: Lanes,
|
||||
) {
|
||||
// Suspense layout effects semantics don't change for legacy roots.
|
||||
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
while (nextEffect !== null) {
|
||||
const fiber = nextEffect;
|
||||
const firstChild = fiber.child;
|
||||
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
// Keep track of the current Offscreen stack's state.
|
||||
if (fiber.tag === OffscreenComponent) {
|
||||
const current = fiber.alternate;
|
||||
const wasHidden = current !== null && current.memoizedState !== null;
|
||||
const isHidden = fiber.memoizedState !== null;
|
||||
|
||||
offscreenSubtreeWasHidden = wasHidden || offscreenSubtreeWasHidden;
|
||||
offscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
|
||||
|
||||
offscreenSubtreeWasHiddenStack.push(wasHidden);
|
||||
offscreenSubtreeIsHiddenStack.push(isHidden);
|
||||
}
|
||||
}
|
||||
|
||||
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
|
||||
ensureCorrectReturnPointer(firstChild, fiber);
|
||||
nextEffect = firstChild;
|
||||
} else {
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
const visibilityChanged =
|
||||
!offscreenSubtreeIsHidden && offscreenSubtreeWasHidden;
|
||||
if (
|
||||
visibilityChanged &&
|
||||
(fiber.subtreeFlags & LayoutStatic) !== NoFlags &&
|
||||
firstChild !== null
|
||||
) {
|
||||
// We've just shown or hidden a Offscreen tree that contains layout effects.
|
||||
// We only enter this code path for subtrees that are updated,
|
||||
// because newly mounted ones would pass the LayoutMask check above.
|
||||
ensureCorrectReturnPointer(firstChild, fiber);
|
||||
nextEffect = firstChild;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
|
||||
}
|
||||
}
|
||||
@@ -2160,9 +2344,76 @@ function commitLayoutMountEffects_complete(
|
||||
root: FiberRoot,
|
||||
committedLanes: Lanes,
|
||||
) {
|
||||
// Suspense layout effects semantics don't change for legacy roots.
|
||||
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
while (nextEffect !== null) {
|
||||
const fiber = nextEffect;
|
||||
if ((fiber.flags & LayoutMask) !== NoFlags) {
|
||||
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
if (fiber.tag === OffscreenComponent) {
|
||||
offscreenSubtreeWasHiddenStack.pop();
|
||||
offscreenSubtreeIsHiddenStack.pop();
|
||||
offscreenSubtreeWasHidden =
|
||||
offscreenSubtreeWasHiddenStack.length > 0 &&
|
||||
offscreenSubtreeWasHiddenStack[
|
||||
offscreenSubtreeWasHiddenStack.length - 1
|
||||
];
|
||||
offscreenSubtreeIsHidden =
|
||||
offscreenSubtreeIsHiddenStack.length > 0 &&
|
||||
offscreenSubtreeIsHiddenStack[
|
||||
offscreenSubtreeIsHiddenStack.length - 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
enableSuspenseLayoutEffectSemantics &&
|
||||
isModernRoot &&
|
||||
offscreenSubtreeWasHidden &&
|
||||
!offscreenSubtreeIsHidden
|
||||
) {
|
||||
// Inside of an Offscreen subtree that changed visibility during this commit.
|
||||
// If this subtree was hidden, layout effects will have already been destroyed (during mutation phase)
|
||||
// but if it was just shown, we need to (re)create the effects now.
|
||||
if ((fiber.flags & LayoutStatic) !== NoFlags) {
|
||||
switch (fiber.tag) {
|
||||
case FunctionComponent:
|
||||
case ForwardRef:
|
||||
case SimpleMemoComponent: {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableProfilerCommitHooks &&
|
||||
fiber.mode & ProfileMode
|
||||
) {
|
||||
try {
|
||||
startLayoutEffectTimer();
|
||||
safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
|
||||
} finally {
|
||||
recordLayoutEffectDuration(fiber);
|
||||
}
|
||||
} else {
|
||||
safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClassComponent: {
|
||||
const instance = fiber.stateNode;
|
||||
safelyCallComponentDidMount(fiber, fiber.return, instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((fiber.flags & RefStatic) !== NoFlags) {
|
||||
switch (fiber.tag) {
|
||||
case ClassComponent:
|
||||
case HostComponent:
|
||||
safelyAttachRef(fiber, fiber.return);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ((fiber.flags & LayoutMask) !== NoFlags) {
|
||||
const current = fiber.alternate;
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
enableScopeAPI,
|
||||
enableStrictEffects,
|
||||
deletedTreeCleanUpLevel,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -79,6 +80,8 @@ import {
|
||||
MutationMask,
|
||||
LayoutMask,
|
||||
PassiveMask,
|
||||
LayoutStatic,
|
||||
RefStatic,
|
||||
} from './ReactFiberFlags';
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
import invariant from 'shared/invariant';
|
||||
@@ -97,7 +100,7 @@ import {
|
||||
recordPassiveEffectDuration,
|
||||
startPassiveEffectTimer,
|
||||
} from './ReactProfilerTimer.old';
|
||||
import {ProfileMode} from './ReactTypeOfMode';
|
||||
import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
|
||||
import {commitUpdateQueue} from './ReactUpdateQueue.old';
|
||||
import {
|
||||
getPublicInstance,
|
||||
@@ -149,6 +152,14 @@ if (__DEV__) {
|
||||
didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
|
||||
}
|
||||
|
||||
// Used during the commit phase to track the state of the Offscreen component stack.
|
||||
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
|
||||
// Only used when enableSuspenseLayoutEffectSemantics is enabled.
|
||||
let offscreenSubtreeIsHidden: boolean = false;
|
||||
const offscreenSubtreeIsHiddenStack: Array<boolean> = [];
|
||||
let offscreenSubtreeWasHidden: boolean = false;
|
||||
const offscreenSubtreeWasHiddenStack: Array<boolean> = [];
|
||||
|
||||
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
|
||||
|
||||
let nextEffect: Fiber | null = null;
|
||||
@@ -172,6 +183,32 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
|
||||
}
|
||||
};
|
||||
|
||||
// Capture errors so they don't interrupt mounting.
|
||||
function safelyCallCommitHookLayoutEffectListMount(
|
||||
current: Fiber,
|
||||
nearestMountedAncestor: Fiber | null,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
commitHookEffectListMount,
|
||||
null,
|
||||
HookLayout,
|
||||
current,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitHookEffectListMount(HookLayout, current);
|
||||
} catch (unmountError) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors so they don't interrupt unmounting.
|
||||
function safelyCallComponentWillUnmount(
|
||||
current: Fiber,
|
||||
@@ -199,6 +236,44 @@ function safelyCallComponentWillUnmount(
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors so they don't interrupt mounting.
|
||||
function safelyCallComponentDidMount(
|
||||
current: Fiber,
|
||||
nearestMountedAncestor: Fiber | null,
|
||||
instance: any,
|
||||
) {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(null, instance.componentDidMount, instance);
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
instance.componentDidMount();
|
||||
} catch (unmountError) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture errors so they don't interrupt mounting.
|
||||
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(null, commitAttachRef, null, current);
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitAttachRef(current);
|
||||
} catch (unmountError) {
|
||||
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
|
||||
const ref = current.ref;
|
||||
if (ref !== null) {
|
||||
@@ -942,6 +1017,12 @@ function commitLayoutEffectOnFiber(
|
||||
}
|
||||
|
||||
function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
// Suspense layout effects semantics don't change for legacy roots.
|
||||
const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
const current = finishedWork.alternate;
|
||||
const wasHidden = current !== null && current.memoizedState !== null;
|
||||
|
||||
if (supportsMutation) {
|
||||
// We only have the top Fiber that was inserted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
@@ -954,6 +1035,25 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
} else {
|
||||
unhideInstance(node.stateNode, node.memoizedProps);
|
||||
}
|
||||
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
// This method is called during mutation; it should detach refs within a hidden subtree.
|
||||
// Attaching refs should be done elsewhere though (during layout).
|
||||
if ((node.flags & RefStatic) !== NoFlags) {
|
||||
if (isHidden) {
|
||||
safelyDetachRef(node, finishedWork);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(node.subtreeFlags & (RefStatic | LayoutStatic)) !== NoFlags &&
|
||||
node.child !== null
|
||||
) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (node.tag === HostText) {
|
||||
const instance = node.stateNode;
|
||||
if (isHidden) {
|
||||
@@ -967,13 +1067,61 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
||||
(node.memoizedState: OffscreenState) !== null &&
|
||||
node !== finishedWork
|
||||
) {
|
||||
// Found a nested Offscreen component that is hidden. Don't search
|
||||
// any deeper. This tree should remain hidden.
|
||||
// Found a nested Offscreen component that is hidden.
|
||||
// Don't search any deeper. This tree should remain hidden.
|
||||
} else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
// When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
|
||||
if ((node.flags & (RefStatic | LayoutStatic)) !== NoFlags) {
|
||||
switch (node.tag) {
|
||||
case FunctionComponent:
|
||||
case ForwardRef:
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent: {
|
||||
// Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
|
||||
if (isHidden && !wasHidden) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableProfilerCommitHooks &&
|
||||
node.mode & ProfileMode
|
||||
) {
|
||||
try {
|
||||
startLayoutEffectTimer();
|
||||
commitHookEffectListUnmount(HookLayout, node, finishedWork);
|
||||
} finally {
|
||||
recordLayoutEffectDuration(node);
|
||||
}
|
||||
} else {
|
||||
commitHookEffectListUnmount(HookLayout, node, finishedWork);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClassComponent: {
|
||||
if (isHidden && !wasHidden) {
|
||||
if ((node.flags & RefStatic) !== NoFlags) {
|
||||
safelyDetachRef(node, finishedWork);
|
||||
}
|
||||
const instance = node.stateNode;
|
||||
if (typeof instance.componentWillUnmount === 'function') {
|
||||
safelyCallComponentWillUnmount(node, finishedWork, instance);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
} else if (node.child !== null) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node === finishedWork) {
|
||||
return;
|
||||
}
|
||||
@@ -2143,13 +2291,49 @@ function commitLayoutEffects_begin(
|
||||
root: FiberRoot,
|
||||
committedLanes: Lanes,
|
||||
) {
|
||||
// Suspense layout effects semantics don't change for legacy roots.
|
||||
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
while (nextEffect !== null) {
|
||||
const fiber = nextEffect;
|
||||
const firstChild = fiber.child;
|
||||
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
// Keep track of the current Offscreen stack's state.
|
||||
if (fiber.tag === OffscreenComponent) {
|
||||
const current = fiber.alternate;
|
||||
const wasHidden = current !== null && current.memoizedState !== null;
|
||||
const isHidden = fiber.memoizedState !== null;
|
||||
|
||||
offscreenSubtreeWasHidden = wasHidden || offscreenSubtreeWasHidden;
|
||||
offscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
|
||||
|
||||
offscreenSubtreeWasHiddenStack.push(wasHidden);
|
||||
offscreenSubtreeIsHiddenStack.push(isHidden);
|
||||
}
|
||||
}
|
||||
|
||||
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
|
||||
ensureCorrectReturnPointer(firstChild, fiber);
|
||||
nextEffect = firstChild;
|
||||
} else {
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
const visibilityChanged =
|
||||
!offscreenSubtreeIsHidden && offscreenSubtreeWasHidden;
|
||||
if (
|
||||
visibilityChanged &&
|
||||
(fiber.subtreeFlags & LayoutStatic) !== NoFlags &&
|
||||
firstChild !== null
|
||||
) {
|
||||
// We've just shown or hidden a Offscreen tree that contains layout effects.
|
||||
// We only enter this code path for subtrees that are updated,
|
||||
// because newly mounted ones would pass the LayoutMask check above.
|
||||
ensureCorrectReturnPointer(firstChild, fiber);
|
||||
nextEffect = firstChild;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
|
||||
}
|
||||
}
|
||||
@@ -2160,9 +2344,76 @@ function commitLayoutMountEffects_complete(
|
||||
root: FiberRoot,
|
||||
committedLanes: Lanes,
|
||||
) {
|
||||
// Suspense layout effects semantics don't change for legacy roots.
|
||||
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
while (nextEffect !== null) {
|
||||
const fiber = nextEffect;
|
||||
if ((fiber.flags & LayoutMask) !== NoFlags) {
|
||||
|
||||
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
|
||||
if (fiber.tag === OffscreenComponent) {
|
||||
offscreenSubtreeWasHiddenStack.pop();
|
||||
offscreenSubtreeIsHiddenStack.pop();
|
||||
offscreenSubtreeWasHidden =
|
||||
offscreenSubtreeWasHiddenStack.length > 0 &&
|
||||
offscreenSubtreeWasHiddenStack[
|
||||
offscreenSubtreeWasHiddenStack.length - 1
|
||||
];
|
||||
offscreenSubtreeIsHidden =
|
||||
offscreenSubtreeIsHiddenStack.length > 0 &&
|
||||
offscreenSubtreeIsHiddenStack[
|
||||
offscreenSubtreeIsHiddenStack.length - 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
enableSuspenseLayoutEffectSemantics &&
|
||||
isModernRoot &&
|
||||
offscreenSubtreeWasHidden &&
|
||||
!offscreenSubtreeIsHidden
|
||||
) {
|
||||
// Inside of an Offscreen subtree that changed visibility during this commit.
|
||||
// If this subtree was hidden, layout effects will have already been destroyed (during mutation phase)
|
||||
// but if it was just shown, we need to (re)create the effects now.
|
||||
if ((fiber.flags & LayoutStatic) !== NoFlags) {
|
||||
switch (fiber.tag) {
|
||||
case FunctionComponent:
|
||||
case ForwardRef:
|
||||
case SimpleMemoComponent: {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableProfilerCommitHooks &&
|
||||
fiber.mode & ProfileMode
|
||||
) {
|
||||
try {
|
||||
startLayoutEffectTimer();
|
||||
safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
|
||||
} finally {
|
||||
recordLayoutEffectDuration(fiber);
|
||||
}
|
||||
} else {
|
||||
safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ClassComponent: {
|
||||
const instance = fiber.stateNode;
|
||||
safelyCallComponentDidMount(fiber, fiber.return, instance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((fiber.flags & RefStatic) !== NoFlags) {
|
||||
switch (fiber.tag) {
|
||||
case ClassComponent:
|
||||
case HostComponent:
|
||||
safelyAttachRef(fiber, fiber.return);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ((fiber.flags & LayoutMask) !== NoFlags) {
|
||||
const current = fiber.alternate;
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
|
||||
import {
|
||||
Ref,
|
||||
RefStatic,
|
||||
Update,
|
||||
NoFlags,
|
||||
DidCapture,
|
||||
@@ -123,6 +124,7 @@ import {
|
||||
enableScopeAPI,
|
||||
enableProfilerTimer,
|
||||
enableCache,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
markSpawnedWork,
|
||||
@@ -157,6 +159,9 @@ function markUpdate(workInProgress: Fiber) {
|
||||
|
||||
function markRef(workInProgress: Fiber) {
|
||||
workInProgress.flags |= Ref;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
workInProgress.flags |= RefStatic;
|
||||
}
|
||||
}
|
||||
|
||||
function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) {
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
|
||||
import {
|
||||
Ref,
|
||||
RefStatic,
|
||||
Update,
|
||||
NoFlags,
|
||||
DidCapture,
|
||||
@@ -123,6 +124,7 @@ import {
|
||||
enableScopeAPI,
|
||||
enableProfilerTimer,
|
||||
enableCache,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
markSpawnedWork,
|
||||
@@ -157,6 +159,9 @@ function markUpdate(workInProgress: Fiber) {
|
||||
|
||||
function markRef(workInProgress: Fiber) {
|
||||
workInProgress.flags |= Ref;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
workInProgress.flags |= RefStatic;
|
||||
}
|
||||
}
|
||||
|
||||
function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) {
|
||||
|
||||
+26
-24
@@ -12,49 +12,51 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
|
||||
export type Flags = number;
|
||||
|
||||
// Don't change these two values. They're used by React Dev Tools.
|
||||
export const NoFlags = /* */ 0b000000000000000000000;
|
||||
export const PerformedWork = /* */ 0b000000000000000000001;
|
||||
export const NoFlags = /* */ 0b00000000000000000000000;
|
||||
export const PerformedWork = /* */ 0b00000000000000000000001;
|
||||
|
||||
// You can change the rest (and add more).
|
||||
export const Placement = /* */ 0b000000000000000000010;
|
||||
export const Update = /* */ 0b000000000000000000100;
|
||||
export const Placement = /* */ 0b00000000000000000000010;
|
||||
export const Update = /* */ 0b00000000000000000000100;
|
||||
export const PlacementAndUpdate = /* */ Placement | Update;
|
||||
export const Deletion = /* */ 0b000000000000000001000;
|
||||
export const ChildDeletion = /* */ 0b000000000000000010000;
|
||||
export const ContentReset = /* */ 0b000000000000000100000;
|
||||
export const Callback = /* */ 0b000000000000001000000;
|
||||
export const DidCapture = /* */ 0b000000000000010000000;
|
||||
export const Ref = /* */ 0b000000000000100000000;
|
||||
export const Snapshot = /* */ 0b000000000001000000000;
|
||||
export const Passive = /* */ 0b000000000010000000000;
|
||||
export const Hydrating = /* */ 0b000000000100000000000;
|
||||
export const Deletion = /* */ 0b00000000000000000001000;
|
||||
export const ChildDeletion = /* */ 0b00000000000000000010000;
|
||||
export const ContentReset = /* */ 0b00000000000000000100000;
|
||||
export const Callback = /* */ 0b00000000000000001000000;
|
||||
export const DidCapture = /* */ 0b00000000000000010000000;
|
||||
export const Ref = /* */ 0b00000000000000100000000;
|
||||
export const Snapshot = /* */ 0b00000000000001000000000;
|
||||
export const Passive = /* */ 0b00000000000010000000000;
|
||||
export const Hydrating = /* */ 0b00000000000100000000000;
|
||||
export const HydratingAndUpdate = /* */ Hydrating | Update;
|
||||
export const Visibility = /* */ 0b000000001000000000000;
|
||||
export const Visibility = /* */ 0b00000000001000000000000;
|
||||
|
||||
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot;
|
||||
|
||||
// Union of all commit flags (flags with the lifetime of a particular commit)
|
||||
export const HostEffectMask = /* */ 0b000000001111111111111;
|
||||
export const HostEffectMask = /* */ 0b00000000001111111111111;
|
||||
|
||||
// These are not really side effects, but we still reuse this field.
|
||||
export const Incomplete = /* */ 0b000000010000000000000;
|
||||
export const ShouldCapture = /* */ 0b000000100000000000000;
|
||||
export const ForceUpdateForLegacySuspense = /* */ 0b000001000000000000000;
|
||||
export const DidPropagateContext = /* */ 0b000010000000000000000;
|
||||
export const NeedsPropagation = /* */ 0b000100000000000000000;
|
||||
export const Incomplete = /* */ 0b00000000010000000000000;
|
||||
export const ShouldCapture = /* */ 0b00000000100000000000000;
|
||||
export const ForceUpdateForLegacySuspense = /* */ 0b00000001000000000000000;
|
||||
export const DidPropagateContext = /* */ 0b00000010000000000000000;
|
||||
export const NeedsPropagation = /* */ 0b00000100000000000000000;
|
||||
|
||||
// Static tags describe aspects of a fiber that are not specific to a render,
|
||||
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
|
||||
// This enables us to defer more work in the unmount case,
|
||||
// since we can defer traversing the tree during layout to look for Passive effects,
|
||||
// and instead rely on the static flag as a signal that there may be cleanup work.
|
||||
export const PassiveStatic = /* */ 0b001000000000000000000;
|
||||
export const RefStatic = /* */ 0b00001000000000000000000;
|
||||
export const LayoutStatic = /* */ 0b00010000000000000000000;
|
||||
export const PassiveStatic = /* */ 0b00100000000000000000000;
|
||||
|
||||
// These flags allow us to traverse to fibers that have effects on mount
|
||||
// without traversing the entire tree after every commit for
|
||||
// double invoking
|
||||
export const MountLayoutDev = /* */ 0b010000000000000000000;
|
||||
export const MountPassiveDev = /* */ 0b100000000000000000000;
|
||||
export const MountLayoutDev = /* */ 0b01000000000000000000000;
|
||||
export const MountPassiveDev = /* */ 0b10000000000000000000000;
|
||||
|
||||
// Groups of flags that are used in the commit phase to skip over trees that
|
||||
// don't contain effects, by checking subtreeFlags.
|
||||
@@ -88,4 +90,4 @@ export const PassiveMask = Passive | ChildDeletion;
|
||||
// Union of tags that don't get reset on clones.
|
||||
// This allows certain concepts to persist without recalculting them,
|
||||
// e.g. whether a subtree contains passive effects or portals.
|
||||
export const StaticMask = PassiveStatic;
|
||||
export const StaticMask = LayoutStatic | PassiveStatic | RefStatic;
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {OpaqueIDType} from './ReactFiberHostConfig';
|
||||
import type {Cache} from './ReactFiberCacheComponent.new';
|
||||
import type {Flags} from './ReactFiberFlags';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
enableUseRefAccessWarning,
|
||||
enableStrictEffects,
|
||||
enableLazyContextPropagation,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {
|
||||
@@ -57,11 +59,13 @@ import {
|
||||
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
|
||||
import {HostRoot, CacheComponent} from './ReactWorkTags';
|
||||
import {
|
||||
Update as UpdateEffect,
|
||||
Passive as PassiveEffect,
|
||||
PassiveStatic as PassiveStaticEffect,
|
||||
LayoutStatic as LayoutStaticEffect,
|
||||
MountLayoutDev as MountLayoutDevEffect,
|
||||
MountPassiveDev as MountPassiveDevEffect,
|
||||
Passive as PassiveEffect,
|
||||
PassiveStatic as PassiveStaticEffect,
|
||||
StaticMask as StaticMaskEffect,
|
||||
Update as UpdateEffect,
|
||||
} from './ReactFiberFlags';
|
||||
import {
|
||||
HasEffect as HookHasEffect,
|
||||
@@ -474,8 +478,8 @@ export function renderWithHooks<Props, SecondArg>(
|
||||
// example, in the SuspenseList implementation.
|
||||
if (
|
||||
current !== null &&
|
||||
(current.flags & PassiveStaticEffect) !==
|
||||
(workInProgress.flags & PassiveStaticEffect)
|
||||
(current.flags & StaticMaskEffect) !==
|
||||
(workInProgress.flags & StaticMaskEffect)
|
||||
) {
|
||||
console.error(
|
||||
'Internal React error: Expected static flag was missing. Please ' +
|
||||
@@ -1478,20 +1482,18 @@ function mountLayoutEffect(
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
): void {
|
||||
let fiberFlags: Flags = UpdateEffect;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStaticEffect;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
return mountEffectImpl(
|
||||
MountLayoutDevEffect | UpdateEffect,
|
||||
HookLayout,
|
||||
create,
|
||||
deps,
|
||||
);
|
||||
} else {
|
||||
return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
|
||||
fiberFlags |= MountLayoutDevEffect;
|
||||
}
|
||||
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
|
||||
}
|
||||
|
||||
function updateLayoutEffect(
|
||||
@@ -1550,25 +1552,23 @@ function mountImperativeHandle<T>(
|
||||
const effectDeps =
|
||||
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
|
||||
|
||||
let fiberFlags: Flags = UpdateEffect;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStaticEffect;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
return mountEffectImpl(
|
||||
MountLayoutDevEffect | UpdateEffect,
|
||||
HookLayout,
|
||||
imperativeHandleEffect.bind(null, create, ref),
|
||||
effectDeps,
|
||||
);
|
||||
} else {
|
||||
return mountEffectImpl(
|
||||
UpdateEffect,
|
||||
HookLayout,
|
||||
imperativeHandleEffect.bind(null, create, ref),
|
||||
effectDeps,
|
||||
);
|
||||
fiberFlags |= MountLayoutDevEffect;
|
||||
}
|
||||
return mountEffectImpl(
|
||||
fiberFlags,
|
||||
HookLayout,
|
||||
imperativeHandleEffect.bind(null, create, ref),
|
||||
effectDeps,
|
||||
);
|
||||
}
|
||||
|
||||
function updateImperativeHandle<T>(
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {OpaqueIDType} from './ReactFiberHostConfig';
|
||||
import type {Cache} from './ReactFiberCacheComponent.old';
|
||||
import type {Flags} from './ReactFiberFlags';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
enableUseRefAccessWarning,
|
||||
enableStrictEffects,
|
||||
enableLazyContextPropagation,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {
|
||||
@@ -57,11 +59,13 @@ import {
|
||||
import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old';
|
||||
import {HostRoot, CacheComponent} from './ReactWorkTags';
|
||||
import {
|
||||
Update as UpdateEffect,
|
||||
Passive as PassiveEffect,
|
||||
PassiveStatic as PassiveStaticEffect,
|
||||
LayoutStatic as LayoutStaticEffect,
|
||||
MountLayoutDev as MountLayoutDevEffect,
|
||||
MountPassiveDev as MountPassiveDevEffect,
|
||||
Passive as PassiveEffect,
|
||||
PassiveStatic as PassiveStaticEffect,
|
||||
StaticMask as StaticMaskEffect,
|
||||
Update as UpdateEffect,
|
||||
} from './ReactFiberFlags';
|
||||
import {
|
||||
HasEffect as HookHasEffect,
|
||||
@@ -474,8 +478,8 @@ export function renderWithHooks<Props, SecondArg>(
|
||||
// example, in the SuspenseList implementation.
|
||||
if (
|
||||
current !== null &&
|
||||
(current.flags & PassiveStaticEffect) !==
|
||||
(workInProgress.flags & PassiveStaticEffect)
|
||||
(current.flags & StaticMaskEffect) !==
|
||||
(workInProgress.flags & StaticMaskEffect)
|
||||
) {
|
||||
console.error(
|
||||
'Internal React error: Expected static flag was missing. Please ' +
|
||||
@@ -1478,20 +1482,18 @@ function mountLayoutEffect(
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
): void {
|
||||
let fiberFlags: Flags = UpdateEffect;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStaticEffect;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
return mountEffectImpl(
|
||||
MountLayoutDevEffect | UpdateEffect,
|
||||
HookLayout,
|
||||
create,
|
||||
deps,
|
||||
);
|
||||
} else {
|
||||
return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
|
||||
fiberFlags |= MountLayoutDevEffect;
|
||||
}
|
||||
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
|
||||
}
|
||||
|
||||
function updateLayoutEffect(
|
||||
@@ -1550,25 +1552,23 @@ function mountImperativeHandle<T>(
|
||||
const effectDeps =
|
||||
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
|
||||
|
||||
let fiberFlags: Flags = UpdateEffect;
|
||||
if (enableSuspenseLayoutEffectSemantics) {
|
||||
fiberFlags |= LayoutStaticEffect;
|
||||
}
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableStrictEffects &&
|
||||
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
|
||||
) {
|
||||
return mountEffectImpl(
|
||||
MountLayoutDevEffect | UpdateEffect,
|
||||
HookLayout,
|
||||
imperativeHandleEffect.bind(null, create, ref),
|
||||
effectDeps,
|
||||
);
|
||||
} else {
|
||||
return mountEffectImpl(
|
||||
UpdateEffect,
|
||||
HookLayout,
|
||||
imperativeHandleEffect.bind(null, create, ref),
|
||||
effectDeps,
|
||||
);
|
||||
fiberFlags |= MountLayoutDevEffect;
|
||||
}
|
||||
return mountEffectImpl(
|
||||
fiberFlags,
|
||||
HookLayout,
|
||||
imperativeHandleEffect.bind(null, create, ref),
|
||||
effectDeps,
|
||||
);
|
||||
}
|
||||
|
||||
function updateImperativeHandle<T>(
|
||||
|
||||
@@ -1271,6 +1271,9 @@ describe('ReactLazy', () => {
|
||||
// @gate enableLazyElements
|
||||
it('mount and reorder lazy types', async () => {
|
||||
class Child extends React.Component {
|
||||
componentWillUnmount() {
|
||||
Scheduler.unstable_yieldValue('Did unmount: ' + this.props.label);
|
||||
}
|
||||
componentDidMount() {
|
||||
Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
|
||||
}
|
||||
@@ -1348,6 +1351,12 @@ describe('ReactLazy', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
|
||||
jest.runAllTimers();
|
||||
|
||||
gate(flags => {
|
||||
if (flags.enableSuspenseLayoutEffectSemantics) {
|
||||
expect(Scheduler).toHaveYielded(['Did unmount: A', 'Did unmount: B']);
|
||||
}
|
||||
});
|
||||
|
||||
// The suspense boundary should've triggered now.
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
await resolveB2({default: ChildB});
|
||||
@@ -1356,12 +1365,23 @@ describe('ReactLazy', () => {
|
||||
expect(Scheduler).toFlushAndYield(['Init A2']);
|
||||
await LazyChildA2;
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'b',
|
||||
'a',
|
||||
'Did update: b',
|
||||
'Did update: a',
|
||||
]);
|
||||
gate(flags => {
|
||||
if (flags.enableSuspenseLayoutEffectSemantics) {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'b',
|
||||
'a',
|
||||
'Did mount: b',
|
||||
'Did mount: a',
|
||||
]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'b',
|
||||
'a',
|
||||
'Did update: b',
|
||||
'Did update: a',
|
||||
]);
|
||||
}
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('ba');
|
||||
});
|
||||
|
||||
|
||||
+3097
File diff suppressed because it is too large
Load Diff
@@ -100,7 +100,7 @@ describe('ReactSuspenseFuzz', () => {
|
||||
}
|
||||
}, [updates]);
|
||||
|
||||
const fullText = `${text}:${step}`;
|
||||
const fullText = `[${text}:${step}]`;
|
||||
|
||||
const shouldSuspend = useContext(ShouldSuspendContext);
|
||||
|
||||
@@ -163,19 +163,26 @@ describe('ReactSuspenseFuzz', () => {
|
||||
resolveAllTasks();
|
||||
const expectedOutput = expectedRoot.getChildrenAsJSX();
|
||||
|
||||
resetCache();
|
||||
ReactNoop.renderLegacySyncRoot(children);
|
||||
resolveAllTasks();
|
||||
const legacyOutput = ReactNoop.getChildrenAsJSX();
|
||||
expect(legacyOutput).toEqual(expectedOutput);
|
||||
ReactNoop.renderLegacySyncRoot(null);
|
||||
gate(flags => {
|
||||
resetCache();
|
||||
ReactNoop.renderLegacySyncRoot(children);
|
||||
resolveAllTasks();
|
||||
const legacyOutput = ReactNoop.getChildrenAsJSX();
|
||||
expect(legacyOutput).toEqual(expectedOutput);
|
||||
ReactNoop.renderLegacySyncRoot(null);
|
||||
|
||||
resetCache();
|
||||
const concurrentRoot = ReactNoop.createRoot();
|
||||
concurrentRoot.render(children);
|
||||
resolveAllTasks();
|
||||
const concurrentOutput = concurrentRoot.getChildrenAsJSX();
|
||||
expect(concurrentOutput).toEqual(expectedOutput);
|
||||
// Observable behavior differs here in a way that's expected:
|
||||
// If enableSuspenseLayoutEffectSemantics is enabled, layout effects are destroyed on re-suspend
|
||||
// before larger 'beginAfter' timers have a chance to fire.
|
||||
if (!flags.enableSuspenseLayoutEffectSemantics) {
|
||||
resetCache();
|
||||
const concurrentRoot = ReactNoop.createRoot();
|
||||
concurrentRoot.render(children);
|
||||
resolveAllTasks();
|
||||
const concurrentOutput = concurrentRoot.getChildrenAsJSX();
|
||||
expect(concurrentOutput).toEqual(expectedOutput);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pickRandomWeighted(rand, options) {
|
||||
@@ -410,5 +417,32 @@ Random seed is ${SEED}
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
it('4', () => {
|
||||
const {Text, testResolvedOutput} = createFuzzer();
|
||||
testResolvedOutput(
|
||||
<React.Suspense fallback="Loading...">
|
||||
<React.Suspense>
|
||||
<React.Suspense>
|
||||
<Text initialDelay={9683} text="E" updates={[]} />
|
||||
</React.Suspense>
|
||||
<Text
|
||||
initialDelay={4053}
|
||||
text="C"
|
||||
updates={[
|
||||
{
|
||||
beginAfter: 1566,
|
||||
suspendFor: 4142,
|
||||
},
|
||||
{
|
||||
beginAfter: 9572,
|
||||
suspendFor: 4832,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</React.Suspense>,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+21
-10
@@ -468,19 +468,30 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
|
||||
await rejectText('Result', new Error('Failed to load: Result'));
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Error! [Result]',
|
||||
gate(flags => {
|
||||
if (flags.enableSuspenseLayoutEffectSemantics) {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Error! [Result]',
|
||||
|
||||
// React retries one more time
|
||||
'Error! [Result]',
|
||||
// React retries one more time
|
||||
'Error! [Result]',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([]);
|
||||
} else {
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Error! [Result]',
|
||||
|
||||
// Errored again on retry. Now handle it.
|
||||
// React retries one more time
|
||||
'Error! [Result]',
|
||||
|
||||
'Caught error: Failed to load: Result',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Caught error: Failed to load: Result'),
|
||||
]);
|
||||
// Errored again on retry. Now handle it.
|
||||
'Caught error: Failed to load: Result',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Caught error: Failed to load: Result'),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// @gate enableCache
|
||||
|
||||
@@ -125,6 +125,11 @@ export const skipUnmountedBoundaries = false;
|
||||
// aggressiveness.
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
|
||||
// Destroy layout effects for components that are hidden because something suspended in an update
|
||||
// and recreate them when they are shown again (after the suspended boundary has resolved).
|
||||
// Note that this should be an uncommon use case and can be avoided by using the transition API.
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
// --------------------------
|
||||
// Future APIs to be deprecated
|
||||
// --------------------------
|
||||
|
||||
@@ -47,6 +47,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = false;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = false;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = false;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = false;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = false;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = false;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
|
||||
export const disableNativeComponentFrames = false;
|
||||
export const skipUnmountedBoundaries = true;
|
||||
export const deletedTreeCleanUpLevel = 1;
|
||||
export const enableSuspenseLayoutEffectSemantics = false;
|
||||
|
||||
export const enableNewReconciler = false;
|
||||
export const deferRenderPhaseUpdateToNextBatch = true;
|
||||
|
||||
@@ -18,6 +18,7 @@ export const disableInputAttributeSyncing = __VARIANT__;
|
||||
export const enableFilterEmptyStringAttributesDOM = __VARIANT__;
|
||||
export const enableLegacyFBSupport = __VARIANT__;
|
||||
export const skipUnmountedBoundaries = __VARIANT__;
|
||||
export const enableSuspenseLayoutEffectSemantics = __VARIANT__;
|
||||
|
||||
// Enable this flag to help with concurrent mode debugging.
|
||||
// It logs information to the console about React scheduling, rendering, and commit phases.
|
||||
|
||||
@@ -28,6 +28,7 @@ export const {
|
||||
skipUnmountedBoundaries,
|
||||
enableStrictEffects,
|
||||
createRootStrictEffectsByDefault,
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
enableUseRefAccessWarning,
|
||||
disableNativeComponentFrames,
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
|
||||
Reference in New Issue
Block a user