diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index 0e723f275d..200e661c70 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -225,18 +225,6 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
}
};
-// Capture errors so they don't interrupt mounting.
-function safelyCallCommitHookLayoutEffectListMount(
- current: Fiber,
- nearestMountedAncestor: Fiber | null,
-) {
- try {
- commitHookEffectListMount(HookLayout, current);
- } catch (error) {
- captureCommitPhaseError(current, nearestMountedAncestor, error);
- }
-}
-
// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(
current: Fiber,
@@ -250,19 +238,6 @@ function safelyCallComponentWillUnmount(
}
}
-// Capture errors so they don't interrupt mounting.
-function safelyCallComponentDidMount(
- current: Fiber,
- nearestMountedAncestor: Fiber | null,
- instance: any,
-) {
- try {
- instance.componentDidMount();
- } catch (error) {
- captureCommitPhaseError(current, nearestMountedAncestor, error);
- }
-}
-
// Capture errors so they don't interrupt mounting.
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
try {
@@ -706,7 +681,7 @@ export function commitPassiveEffectDurations(
}
}
-function commitHookLayoutEffects(finishedWork: Fiber) {
+function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
@@ -718,14 +693,14 @@ function commitHookLayoutEffects(finishedWork: Fiber) {
) {
try {
startLayoutEffectTimer();
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
+ commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
+ commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
@@ -853,7 +828,7 @@ function commitClassLayoutLifecycles(
}
}
-function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
+function commitClassCallbacks(finishedWork: Fiber) {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
@@ -897,7 +872,7 @@ function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
}
}
-function commitHostComponentMount(finishedWork: Fiber, current: Fiber | null) {
+function commitHostComponentMount(finishedWork: Fiber) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
const instance: Instance = finishedWork.stateNode;
@@ -978,6 +953,8 @@ function commitLayoutEffectOnFiber(
finishedWork: Fiber,
committedLanes: Lanes,
): void {
+ // When updating this function, also update reappearLayoutEffects, which does
+ // most of the same things when an offscreen tree goes from hidden -> visible.
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
@@ -989,9 +966,7 @@ function commitLayoutEffectOnFiber(
committedLanes,
);
if (flags & Update) {
- if (!offscreenSubtreeWasHidden) {
- commitHookLayoutEffects(finishedWork);
- }
+ commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
break;
}
@@ -1002,19 +977,15 @@ function commitLayoutEffectOnFiber(
committedLanes,
);
if (flags & Update) {
- if (!offscreenSubtreeWasHidden) {
- commitClassLayoutLifecycles(finishedWork, current);
- }
+ commitClassLayoutLifecycles(finishedWork, current);
}
if (flags & Callback) {
- commitClassCallbacks(finishedWork, current);
+ commitClassCallbacks(finishedWork);
}
if (flags & Ref) {
- if (!offscreenSubtreeWasHidden) {
- safelyAttachRef(finishedWork, finishedWork.return);
- }
+ safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
@@ -1063,13 +1034,11 @@ function commitLayoutEffectOnFiber(
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (current === null && flags & Update) {
- commitHostComponentMount(finishedWork, current);
+ commitHostComponentMount(finishedWork);
}
if (flags & Ref) {
- if (!offscreenSubtreeWasHidden) {
- safelyAttachRef(finishedWork, finishedWork.return);
- }
+ safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
@@ -1117,19 +1086,25 @@ function commitLayoutEffectOnFiber(
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
- // This is the root of a reappearing boundary. Turn its layout
- // effects back on.
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ // This is the root of a reappearing boundary. As we continue
+ // traversing the layout effects, we must also re-mount layout
+ // effects that were unmounted when the Offscreen subtree was
+ // hidden. So this is a superset of the normal commitLayoutEffects.
+ const includeWorkInProgressEffects =
+ (finishedWork.subtreeFlags & LayoutMask) !== NoFlags;
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+ } else {
+ recursivelyTraverseLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ );
}
-
- // TODO: We shouldn't traverse twice when reappearing layout effects.
- // Move this into the else block of the above if statement, and modify
- // reappearLayoutEffects to fire regular layout effects, too.
- recursivelyTraverseLayoutEffects(
- finishedRoot,
- finishedWork,
- committedLanes,
- );
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
}
@@ -2723,90 +2698,177 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
}
}
-function reappearLayoutEffects(finishedWork: Fiber) {
+function reappearLayoutEffects(
+ finishedRoot: FiberRoot,
+ current: Fiber | null,
+ finishedWork: Fiber,
+ committedLanes: Lanes,
+ // This function visits both newly finished work and nodes that were re-used
+ // from a previously committed tree. We cannot check non-static flags if the
+ // node was reused.
+ includeWorkInProgressEffects: boolean,
+) {
// Turn on layout effects in a tree that previously disappeared.
- // TODO (Offscreen) Check: flags & LayoutStatic
+ const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
-
- // TODO: Check for LayoutStatic flag
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
- try {
- startLayoutEffectTimer();
- safelyCallCommitHookLayoutEffectListMount(
- finishedWork,
- finishedWork.return,
- );
- } finally {
- recordLayoutEffectDuration(finishedWork);
- }
- } else {
- safelyCallCommitHookLayoutEffectListMount(
- finishedWork,
- finishedWork.return,
- );
- }
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+ // TODO: Check flags & LayoutStatic
+ commitHookLayoutEffects(finishedWork, HookLayout);
break;
}
case ClassComponent: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
- const instance = finishedWork.stateNode;
// TODO: Check for LayoutStatic flag
+ const instance = finishedWork.stateNode;
if (typeof instance.componentDidMount === 'function') {
- safelyCallComponentDidMount(
- finishedWork,
- finishedWork.return,
- instance,
- );
+ try {
+ instance.componentDidMount();
+ } catch (error) {
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
+ }
}
- // TODO: Check for RefStatic flag
- safelyAttachRef(finishedWork, finishedWork.return);
+
+ // Commit any callbacks that would have fired while the component
+ // was hidden.
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
commitHiddenCallbacks(updateQueue, instance);
}
- break;
- }
- case HostComponent: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
- // TODO: Check for RefStatic flag
+ // If this is newly finished work, check for setState callbacks
+ if (includeWorkInProgressEffects && flags & Callback) {
+ commitClassCallbacks(finishedWork);
+ }
+
+ // TODO: Check flags & RefStatic
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
+ // Unlike commitLayoutEffectsOnFiber, we don't need to handle HostRoot
+ // because this function only visits nodes that are inside an
+ // Offscreen fiber.
+ // case HostRoot: {
+ // ...
+ // }
+ case HostComponent: {
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+
+ // Renderers may schedule work to be done after host components are mounted
+ // (eg DOM renderer may schedule auto-focus for inputs and form controls).
+ // These effects should only be committed when components are first mounted,
+ // aka when there is no current/alternate.
+ if (includeWorkInProgressEffects && current === null && flags & Update) {
+ commitHostComponentMount(finishedWork);
+ }
+
+ // TODO: Check flags & Ref
+ safelyAttachRef(finishedWork, finishedWork.return);
+ break;
+ }
+ case Profiler: {
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+ // TODO: Figure out how Profiler updates should work with Offscreen
+ if (includeWorkInProgressEffects && flags & Update) {
+ commitProfilerUpdate(finishedWork, current);
+ }
+ break;
+ }
+ case SuspenseComponent: {
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+
+ // TODO: Figure out how Suspense hydration callbacks should work
+ // with Offscreen.
+ if (includeWorkInProgressEffects && flags & Update) {
+ commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
+ }
+ break;
+ }
case OffscreenComponent: {
- const isHidden = finishedWork.memoizedState !== null;
+ const offscreenState: OffscreenState = finishedWork.memoizedState;
+ const isHidden = offscreenState !== null;
if (isHidden) {
// Nested Offscreen tree is still hidden. Don't re-appear its effects.
} else {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
}
break;
}
default: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
break;
}
}
}
-function recursivelyTraverseReappearLayoutEffects(parentFiber: Fiber) {
+function recursivelyTraverseReappearLayoutEffects(
+ finishedRoot: FiberRoot,
+ parentFiber: Fiber,
+ committedLanes: Lanes,
+ includeWorkInProgressEffects: boolean,
+) {
+ // This function visits both newly finished work and nodes that were re-used
+ // from a previously committed tree. We cannot check non-static flags if the
+ // node was reused.
+ const childShouldIncludeWorkInProgressEffects =
+ includeWorkInProgressEffects &&
+ (parentFiber.subtreeFlags & LayoutMask) !== NoFlags;
+
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
+ const prevDebugFiber = getCurrentDebugFiberInDEV();
let child = parentFiber.child;
while (child !== null) {
- reappearLayoutEffects(child);
+ const current = child.alternate;
+ reappearLayoutEffects(
+ finishedRoot,
+ current,
+ child,
+ committedLanes,
+ childShouldIncludeWorkInProgressEffects,
+ );
child = child.sibling;
}
+ setCurrentDebugFiberInDEV(prevDebugFiber);
}
export function commitPassiveMountEffects(
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index 13ae7fef32..4b979151da 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -225,18 +225,6 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
}
};
-// Capture errors so they don't interrupt mounting.
-function safelyCallCommitHookLayoutEffectListMount(
- current: Fiber,
- nearestMountedAncestor: Fiber | null,
-) {
- try {
- commitHookEffectListMount(HookLayout, current);
- } catch (error) {
- captureCommitPhaseError(current, nearestMountedAncestor, error);
- }
-}
-
// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(
current: Fiber,
@@ -250,19 +238,6 @@ function safelyCallComponentWillUnmount(
}
}
-// Capture errors so they don't interrupt mounting.
-function safelyCallComponentDidMount(
- current: Fiber,
- nearestMountedAncestor: Fiber | null,
- instance: any,
-) {
- try {
- instance.componentDidMount();
- } catch (error) {
- captureCommitPhaseError(current, nearestMountedAncestor, error);
- }
-}
-
// Capture errors so they don't interrupt mounting.
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
try {
@@ -706,7 +681,7 @@ export function commitPassiveEffectDurations(
}
}
-function commitHookLayoutEffects(finishedWork: Fiber) {
+function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
@@ -718,14 +693,14 @@ function commitHookLayoutEffects(finishedWork: Fiber) {
) {
try {
startLayoutEffectTimer();
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
+ commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
+ commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
@@ -853,7 +828,7 @@ function commitClassLayoutLifecycles(
}
}
-function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
+function commitClassCallbacks(finishedWork: Fiber) {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
@@ -897,7 +872,7 @@ function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
}
}
-function commitHostComponentMount(finishedWork: Fiber, current: Fiber | null) {
+function commitHostComponentMount(finishedWork: Fiber) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
const instance: Instance = finishedWork.stateNode;
@@ -978,6 +953,8 @@ function commitLayoutEffectOnFiber(
finishedWork: Fiber,
committedLanes: Lanes,
): void {
+ // When updating this function, also update reappearLayoutEffects, which does
+ // most of the same things when an offscreen tree goes from hidden -> visible.
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
@@ -989,9 +966,7 @@ function commitLayoutEffectOnFiber(
committedLanes,
);
if (flags & Update) {
- if (!offscreenSubtreeWasHidden) {
- commitHookLayoutEffects(finishedWork);
- }
+ commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
break;
}
@@ -1002,19 +977,15 @@ function commitLayoutEffectOnFiber(
committedLanes,
);
if (flags & Update) {
- if (!offscreenSubtreeWasHidden) {
- commitClassLayoutLifecycles(finishedWork, current);
- }
+ commitClassLayoutLifecycles(finishedWork, current);
}
if (flags & Callback) {
- commitClassCallbacks(finishedWork, current);
+ commitClassCallbacks(finishedWork);
}
if (flags & Ref) {
- if (!offscreenSubtreeWasHidden) {
- safelyAttachRef(finishedWork, finishedWork.return);
- }
+ safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
@@ -1063,13 +1034,11 @@ function commitLayoutEffectOnFiber(
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (current === null && flags & Update) {
- commitHostComponentMount(finishedWork, current);
+ commitHostComponentMount(finishedWork);
}
if (flags & Ref) {
- if (!offscreenSubtreeWasHidden) {
- safelyAttachRef(finishedWork, finishedWork.return);
- }
+ safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
@@ -1117,19 +1086,25 @@ function commitLayoutEffectOnFiber(
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
- // This is the root of a reappearing boundary. Turn its layout
- // effects back on.
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ // This is the root of a reappearing boundary. As we continue
+ // traversing the layout effects, we must also re-mount layout
+ // effects that were unmounted when the Offscreen subtree was
+ // hidden. So this is a superset of the normal commitLayoutEffects.
+ const includeWorkInProgressEffects =
+ (finishedWork.subtreeFlags & LayoutMask) !== NoFlags;
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+ } else {
+ recursivelyTraverseLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ );
}
-
- // TODO: We shouldn't traverse twice when reappearing layout effects.
- // Move this into the else block of the above if statement, and modify
- // reappearLayoutEffects to fire regular layout effects, too.
- recursivelyTraverseLayoutEffects(
- finishedRoot,
- finishedWork,
- committedLanes,
- );
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
}
@@ -2723,90 +2698,177 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
}
}
-function reappearLayoutEffects(finishedWork: Fiber) {
+function reappearLayoutEffects(
+ finishedRoot: FiberRoot,
+ current: Fiber | null,
+ finishedWork: Fiber,
+ committedLanes: Lanes,
+ // This function visits both newly finished work and nodes that were re-used
+ // from a previously committed tree. We cannot check non-static flags if the
+ // node was reused.
+ includeWorkInProgressEffects: boolean,
+) {
// Turn on layout effects in a tree that previously disappeared.
- // TODO (Offscreen) Check: flags & LayoutStatic
+ const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
-
- // TODO: Check for LayoutStatic flag
- if (
- enableProfilerTimer &&
- enableProfilerCommitHooks &&
- finishedWork.mode & ProfileMode
- ) {
- try {
- startLayoutEffectTimer();
- safelyCallCommitHookLayoutEffectListMount(
- finishedWork,
- finishedWork.return,
- );
- } finally {
- recordLayoutEffectDuration(finishedWork);
- }
- } else {
- safelyCallCommitHookLayoutEffectListMount(
- finishedWork,
- finishedWork.return,
- );
- }
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+ // TODO: Check flags & LayoutStatic
+ commitHookLayoutEffects(finishedWork, HookLayout);
break;
}
case ClassComponent: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
- const instance = finishedWork.stateNode;
// TODO: Check for LayoutStatic flag
+ const instance = finishedWork.stateNode;
if (typeof instance.componentDidMount === 'function') {
- safelyCallComponentDidMount(
- finishedWork,
- finishedWork.return,
- instance,
- );
+ try {
+ instance.componentDidMount();
+ } catch (error) {
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
+ }
}
- // TODO: Check for RefStatic flag
- safelyAttachRef(finishedWork, finishedWork.return);
+
+ // Commit any callbacks that would have fired while the component
+ // was hidden.
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
commitHiddenCallbacks(updateQueue, instance);
}
- break;
- }
- case HostComponent: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
- // TODO: Check for RefStatic flag
+ // If this is newly finished work, check for setState callbacks
+ if (includeWorkInProgressEffects && flags & Callback) {
+ commitClassCallbacks(finishedWork);
+ }
+
+ // TODO: Check flags & RefStatic
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
+ // Unlike commitLayoutEffectsOnFiber, we don't need to handle HostRoot
+ // because this function only visits nodes that are inside an
+ // Offscreen fiber.
+ // case HostRoot: {
+ // ...
+ // }
+ case HostComponent: {
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+
+ // Renderers may schedule work to be done after host components are mounted
+ // (eg DOM renderer may schedule auto-focus for inputs and form controls).
+ // These effects should only be committed when components are first mounted,
+ // aka when there is no current/alternate.
+ if (includeWorkInProgressEffects && current === null && flags & Update) {
+ commitHostComponentMount(finishedWork);
+ }
+
+ // TODO: Check flags & Ref
+ safelyAttachRef(finishedWork, finishedWork.return);
+ break;
+ }
+ case Profiler: {
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+ // TODO: Figure out how Profiler updates should work with Offscreen
+ if (includeWorkInProgressEffects && flags & Update) {
+ commitProfilerUpdate(finishedWork, current);
+ }
+ break;
+ }
+ case SuspenseComponent: {
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
+
+ // TODO: Figure out how Suspense hydration callbacks should work
+ // with Offscreen.
+ if (includeWorkInProgressEffects && flags & Update) {
+ commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
+ }
+ break;
+ }
case OffscreenComponent: {
- const isHidden = finishedWork.memoizedState !== null;
+ const offscreenState: OffscreenState = finishedWork.memoizedState;
+ const isHidden = offscreenState !== null;
if (isHidden) {
// Nested Offscreen tree is still hidden. Don't re-appear its effects.
} else {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
}
break;
}
default: {
- recursivelyTraverseReappearLayoutEffects(finishedWork);
+ recursivelyTraverseReappearLayoutEffects(
+ finishedRoot,
+ finishedWork,
+ committedLanes,
+ includeWorkInProgressEffects,
+ );
break;
}
}
}
-function recursivelyTraverseReappearLayoutEffects(parentFiber: Fiber) {
+function recursivelyTraverseReappearLayoutEffects(
+ finishedRoot: FiberRoot,
+ parentFiber: Fiber,
+ committedLanes: Lanes,
+ includeWorkInProgressEffects: boolean,
+) {
+ // This function visits both newly finished work and nodes that were re-used
+ // from a previously committed tree. We cannot check non-static flags if the
+ // node was reused.
+ const childShouldIncludeWorkInProgressEffects =
+ includeWorkInProgressEffects &&
+ (parentFiber.subtreeFlags & LayoutMask) !== NoFlags;
+
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
+ const prevDebugFiber = getCurrentDebugFiberInDEV();
let child = parentFiber.child;
while (child !== null) {
- reappearLayoutEffects(child);
+ const current = child.alternate;
+ reappearLayoutEffects(
+ finishedRoot,
+ current,
+ child,
+ committedLanes,
+ childShouldIncludeWorkInProgressEffects,
+ );
child = child.sibling;
}
+ setCurrentDebugFiberInDEV(prevDebugFiber);
}
export function commitPassiveMountEffects(
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
index 1df22cd76b..c2fe2f7755 100644
--- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
@@ -696,4 +696,175 @@ describe('ReactOffscreen', () => {
]);
expect(root).toMatchRenderedOutput();
});
+
+ // @gate enableOffscreen
+ it('does not call componentDidUpdate when reappearing a hidden class component', async () => {
+ class Child extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('componentDidMount');
+ }
+ componentDidUpdate() {
+ Scheduler.unstable_yieldValue('componentDidUpdate');
+ }
+ componentWillUnmount() {
+ Scheduler.unstable_yieldValue('componentWillUnmount');
+ }
+ render() {
+ return 'Child';
+ }
+ }
+
+ // Initial mount
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['componentDidMount']);
+
+ // Hide the class component
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['componentWillUnmount']);
+
+ // Reappear the class component. componentDidMount should fire, not
+ // componentDidUpdate.
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['componentDidMount']);
+ });
+
+ // @gate enableOffscreen
+ it(
+ 'when reusing old components (hidden -> visible), layout effects fire ' +
+ 'with same timing as if it were brand new',
+ async () => {
+ function Child({label}) {
+ useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Mount ' + label);
+ return () => {
+ Scheduler.unstable_yieldValue('Unmount ' + label);
+ };
+ }, [label]);
+ return label;
+ }
+
+ // Initial mount
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['Mount B']);
+
+ // Hide the component
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['Unmount B']);
+
+ // Reappear the component and also add some new siblings.
+ await act(async () => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ // B's effect should fire in between A and C even though it's been reused
+ // from a previous render. In other words, it's the same order as if all
+ // three siblings were brand new.
+ expect(Scheduler).toHaveYielded(['Mount A', 'Mount B', 'Mount C']);
+ },
+ );
+
+ // @gate enableOffscreen
+ it(
+ 'when reusing old components (hidden -> visible), layout effects fire ' +
+ 'with same timing as if it were brand new (includes setState callback)',
+ async () => {
+ class Child extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Mount ' + this.props.label);
+ }
+ componentWillUnmount() {
+ Scheduler.unstable_yieldValue('Unmount ' + this.props.label);
+ }
+ render() {
+ return this.props.label;
+ }
+ }
+
+ // Initial mount
+ const bRef = React.createRef();
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['Mount B']);
+
+ // We're going to schedule an update on a hidden component, so stash a
+ // reference to its setState before the ref gets detached
+ const setStateB = bRef.current.setState.bind(bRef.current);
+
+ // Hide the component
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['Unmount B']);
+
+ // Reappear the component and also add some new siblings.
+ await act(async () => {
+ setStateB(null, () => {
+ Scheduler.unstable_yieldValue('setState callback B');
+ });
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ // B's effect should fire in between A and C even though it's been reused
+ // from a previous render. In other words, it's the same order as if all
+ // three siblings were brand new.
+ expect(Scheduler).toHaveYielded([
+ 'Mount A',
+ 'Mount B',
+ 'setState callback B',
+ 'Mount C',
+ ]);
+ },
+ );
});