diff --git a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js index ca5c9fac5162..f4303d68c0f4 100644 --- a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js +++ b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js @@ -153,51 +153,6 @@ function resolveMostRecentTextCache(text) { const resolveText = resolveMostRecentTextCache; -// Don't feel too guilty if you have to delete this test. -// @gate dfsEffectsRefactor -// @gate __DEV__ -test('warns in DEV if return pointer is inconsistent', async () => { - const {useRef, useLayoutEffect} = React; - - let ref = null; - function App({text}) { - ref = useRef(null); - return ( - <> - -
{text}
- - ); - } - - function Sibling({text}) { - useLayoutEffect(() => { - if (text === 'B') { - // Mutate the return pointer of the div to point to the wrong alternate. - // This simulates the most common type of return pointer inconsistency. - const current = ref.current.fiber; - const workInProgress = current.alternate; - workInProgress.return = current.return; - } - }, [text]); - return null; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render(); - }); - - spyOnDev(console, 'error'); - await act(async () => { - root.render(); - }); - expect(console.error.calls.count()).toBe(1); - expect(console.error.calls.argsFor(0)[0]).toMatch( - 'Internal React error: Return pointer is inconsistent with parent.', - ); -}); - // @gate enableCache // @gate enableSuspenseList test('regression (#20932): return pointer is correct before entering deleted tree', async () => { diff --git a/packages/react-reconciler/src/ReactCurrentFiber.js b/packages/react-reconciler/src/ReactCurrentFiber.js index b2186131a98a..08853b384bc6 100644 --- a/packages/react-reconciler/src/ReactCurrentFiber.js +++ b/packages/react-reconciler/src/ReactCurrentFiber.js @@ -51,14 +51,22 @@ export function resetCurrentFiber() { } } -export function setCurrentFiber(fiber: Fiber) { +export function setCurrentFiber(fiber: Fiber | null) { if (__DEV__) { - ReactDebugCurrentFrame.getCurrentStack = getCurrentFiberStackInDev; + ReactDebugCurrentFrame.getCurrentStack = + fiber === null ? null : getCurrentFiberStackInDev; current = fiber; isRendering = false; } } +export function getCurrentFiber(): Fiber | null { + if (__DEV__) { + return current; + } + return null; +} + export function setIsRendering(rendering: boolean) { if (__DEV__) { isRendering = rendering; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 6d3b0e555a64..8f7741f74ee1 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -68,13 +68,11 @@ import { NoFlags, ContentReset, Placement, - PlacementAndUpdate, ChildDeletion, Snapshot, Update, Ref, Hydrating, - HydratingAndUpdate, Passive, BeforeMutationMask, MutationMask, @@ -86,6 +84,7 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom import { resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, + getCurrentFiber as getCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; import { @@ -182,7 +181,7 @@ let nextEffect: Fiber | null = null; let inProgressLanes: Lanes | null = null; let inProgressRoot: FiberRoot | null = null; -function reportUncaughtErrorInDEV(error) { +export function reportUncaughtErrorInDEV(error: mixed) { // Wrapping each small part of the commit phase into a guarded // callback is a bit too slow (https://github.com/facebook/react/pull/21666). // But we rely on it to surface errors to DEV tools like overlays @@ -223,7 +222,6 @@ function safelyCallCommitHookLayoutEffectListMount( try { commitHookEffectListMount(HookLayout, current); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -237,7 +235,6 @@ function safelyCallComponentWillUnmount( try { callComponentWillUnmountWithTimer(current, instance); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -251,7 +248,6 @@ function safelyCallComponentDidMount( try { instance.componentDidMount(); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -261,7 +257,6 @@ function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { try { commitAttachRef(current); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -287,7 +282,6 @@ function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { retVal = ref(null); } } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } if (__DEV__) { @@ -313,7 +307,6 @@ function safelyCallDestroy( try { destroy(); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -360,7 +353,7 @@ function commitBeforeMutationEffects_begin() { (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null ) { - ensureCorrectReturnPointer(child, fiber); + child.return = fiber; nextEffect = child; } else { commitBeforeMutationEffects_complete(); @@ -375,14 +368,13 @@ function commitBeforeMutationEffects_complete() { try { commitBeforeMutationEffectsOnFiber(fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -1086,21 +1078,28 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) { if (node.tag === HostComponent) { if (hostSubtreeRoot === null) { hostSubtreeRoot = node; - - const instance = node.stateNode; - if (isHidden) { - hideInstance(instance); - } else { - unhideInstance(node.stateNode, node.memoizedProps); + try { + const instance = node.stateNode; + if (isHidden) { + hideInstance(instance); + } else { + unhideInstance(node.stateNode, node.memoizedProps); + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); } } } else if (node.tag === HostText) { if (hostSubtreeRoot === null) { - const instance = node.stateNode; - if (isHidden) { - hideTextInstance(instance); - } else { - unhideTextInstance(instance, node.memoizedProps); + try { + const instance = node.stateNode; + if (isHidden) { + hideTextInstance(instance); + } else { + unhideTextInstance(instance, node.memoizedProps); + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); } } } else if ( @@ -1223,148 +1222,6 @@ function commitDetachRef(current: Fiber) { } } -// User-originating errors (lifecycles and refs) should not interrupt -// deletion, so don't let them throw. Host-originating errors should -// interrupt deletion, so it's okay -function commitUnmount( - finishedRoot: FiberRoot, - current: Fiber, - nearestMountedAncestor: Fiber, -): void { - onCommitUnmount(current); - - switch (current.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); - if (updateQueue !== null) { - const lastEffect = updateQueue.lastEffect; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - - let effect = firstEffect; - do { - const {destroy, tag} = effect; - if (destroy !== undefined) { - if ((tag & HookInsertion) !== NoHookEffect) { - safelyCallDestroy(current, nearestMountedAncestor, destroy); - } else if ((tag & HookLayout) !== NoHookEffect) { - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStarted(current); - } - - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - startLayoutEffectTimer(); - safelyCallDestroy(current, nearestMountedAncestor, destroy); - recordLayoutEffectDuration(current); - } else { - safelyCallDestroy(current, nearestMountedAncestor, destroy); - } - - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStopped(); - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } - } - return; - } - case ClassComponent: { - safelyDetachRef(current, nearestMountedAncestor); - const instance = current.stateNode; - if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount( - current, - nearestMountedAncestor, - instance, - ); - } - return; - } - case HostComponent: { - safelyDetachRef(current, nearestMountedAncestor); - return; - } - case HostPortal: { - // TODO: this is recursive. - // We are also not using this parent because - // the portal will get pushed immediately. - if (supportsMutation) { - unmountHostComponents(finishedRoot, current, nearestMountedAncestor); - } else if (supportsPersistence) { - emptyPortalContainer(current); - } - return; - } - case DehydratedFragment: { - if (enableSuspenseCallback) { - const hydrationCallbacks = finishedRoot.hydrationCallbacks; - if (hydrationCallbacks !== null) { - const onDeleted = hydrationCallbacks.onDeleted; - if (onDeleted) { - onDeleted((current.stateNode: SuspenseInstance)); - } - } - } - return; - } - case ScopeComponent: { - if (enableScopeAPI) { - safelyDetachRef(current, nearestMountedAncestor); - } - return; - } - } -} - -function commitNestedUnmounts( - finishedRoot: FiberRoot, - root: Fiber, - nearestMountedAncestor: Fiber, -): void { - // While we're inside a removed host node we don't want to call - // removeChild on the inner nodes because they're removed by the top - // call anyway. We also want to call componentWillUnmount on all - // composites before this host node is removed from the tree. Therefore - // we do an inner loop while we're still inside the host node. - let node: Fiber = root; - while (true) { - commitUnmount(finishedRoot, node, nearestMountedAncestor); - // Visit children because they may contain more composite or host nodes. - // Skip portals because commitUnmount() currently visits them recursively. - if ( - node.child !== null && - // If we use mutation we drill down into portals using commitUnmount above. - // If we don't use mutation we drill down into portals here instead. - (!supportsMutation || node.tag !== HostPortal) - ) { - node.child.return = node; - node = node.child; - continue; - } - if (node === root) { - return; - } - while (node.sibling === null) { - if (node.return === null || node.return === root) { - return; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - } -} - function detachFiberMutation(fiber: Fiber) { // Cut off the return pointer to disconnect it from the tree. // This enables us to detect and warn against state updates on an unmounted component. @@ -1481,36 +1338,6 @@ function emptyPortalContainer(current: Fiber) { replaceContainerChildren(containerInfo, emptyChildSet); } -function commitContainer(finishedWork: Fiber) { - if (!supportsPersistence) { - return; - } - - switch (finishedWork.tag) { - case ClassComponent: - case HostComponent: - case HostText: { - return; - } - case HostRoot: - case HostPortal: { - const portalOrRoot: { - containerInfo: Container, - pendingChildren: ChildSet, - ... - } = finishedWork.stateNode; - const {containerInfo, pendingChildren} = portalOrRoot; - replaceContainerChildren(containerInfo, pendingChildren); - return; - } - } - - throw new Error( - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); -} - function getHostParentFiber(fiber: Fiber): Fiber { let parent = fiber.return; while (parent !== null) { @@ -1683,388 +1510,353 @@ function insertOrAppendPlacementNode( } } -function unmountHostComponents( - finishedRoot: FiberRoot, - current: Fiber, - nearestMountedAncestor: Fiber, -): void { - // We only have the top Fiber that was deleted but we need to recurse down its - // children to find all the terminal nodes. - let node: Fiber = current; +// These are tracked on the stack as we recursively traverse a +// deleted subtree. +// TODO: Update these during the whole mutation phase, not just during +// a deletion. +let hostParent: Instance | Container | null = null; +let hostParentIsContainer: boolean = false; - // Each iteration, currentParent is populated with node's host parent if not - // currentParentIsValid. - let currentParentIsValid = false; +function commitDeletionEffects( + root: FiberRoot, + returnFiber: Fiber, + deletedFiber: Fiber, +) { + if (supportsMutation) { + // We only have the top Fiber that was deleted but we need to recurse down its + // children to find all the terminal nodes. - // Note: these two variables *must* always be updated together. - let currentParent; - let currentParentIsContainer; - - while (true) { - if (!currentParentIsValid) { - let parent = node.return; - findParent: while (true) { - if (parent === null) { - throw new Error( - 'Expected to find a host parent. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - } + // Recursively delete all host nodes from the parent, detach refs, clean + // up mounted layout effects, and call componentWillUnmount. + + // We only need to remove the topmost host child in each branch. But then we + // still need to keep traversing to unmount effects, refs, and cWU. TODO: We + // could split this into two separate traversals functions, where the second + // one doesn't include any removeChild logic. This is maybe the same + // function as "disappearLayoutEffects" (or whatever that turns into after + // the layout phase is refactored to use recursion). + + // Before starting, find the nearest host parent on the stack so we know + // which instance/container to remove the children from. + // TODO: Instead of searching up the fiber return path on every deletion, we + // can track the nearest host component on the JS stack as we traverse the + // tree during the commit phase. This would make insertions faster, too. + let parent = returnFiber; + findParent: while (parent !== null) { + switch (parent.tag) { + case HostComponent: { + hostParent = parent.stateNode; + hostParentIsContainer = false; + break findParent; + } + case HostRoot: { + hostParent = parent.stateNode.containerInfo; + hostParentIsContainer = true; + break findParent; + } + case HostPortal: { + hostParent = parent.stateNode.containerInfo; + hostParentIsContainer = true; + break findParent; + } + } + parent = parent.return; + } + if (hostParent === null) { + throw new Error( + 'Expected to find a host parent. This error is likely caused by ' + + 'a bug in React. Please file an issue.', + ); + } + commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); + hostParent = null; + hostParentIsContainer = false; + } else { + // Detach refs and call componentWillUnmount() on the whole subtree. + commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); + } + + detachFiberMutation(deletedFiber); +} + +function recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + parent, +) { + // TODO: Use a static flag to skip trees that don't have unmount effects + let child = parent.child; + while (child !== null) { + commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child); + child = child.sibling; + } +} + +function commitDeletionEffectsOnFiber( + finishedRoot: FiberRoot, + nearestMountedAncestor: Fiber, + deletedFiber: Fiber, +) { + onCommitUnmount(deletedFiber); - const parentStateNode = parent.stateNode; - switch (parent.tag) { - case HostComponent: - currentParent = parentStateNode; - currentParentIsContainer = false; - break findParent; - case HostRoot: - currentParent = parentStateNode.containerInfo; - currentParentIsContainer = true; - break findParent; - case HostPortal: - currentParent = parentStateNode.containerInfo; - currentParentIsContainer = true; - break findParent; - } - parent = parent.return; - } - currentParentIsValid = true; - } - - if (node.tag === HostComponent || node.tag === HostText) { - commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor); - // After all the children have unmounted, it is now safe to remove the - // node from the tree. - if (currentParentIsContainer) { - removeChildFromContainer( - ((currentParent: any): Container), - (node.stateNode: Instance | TextInstance), + // The cases in this outer switch modify the stack before they traverse + // into their subtree. There are simpler cases in the inner switch + // that don't modify the stack. + switch (deletedFiber.tag) { + case HostComponent: { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + // Intentional fallthrough to next branch + } + // eslint-disable-next-line-no-fallthrough + case HostText: { + // We only need to remove the nearest host child. Set the host parent + // to `null` on the stack to indicate that nested children don't + // need to be removed. + if (supportsMutation) { + const prevHostParent = hostParent; + const prevHostParentIsContainer = hostParentIsContainer; + hostParent = null; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); + hostParent = prevHostParent; + hostParentIsContainer = prevHostParentIsContainer; + + if (hostParent !== null) { + // Now that all the child effects have unmounted, we can remove the + // node from the tree. + if (hostParentIsContainer) { + removeChildFromContainer( + ((hostParent: any): Container), + (deletedFiber.stateNode: Instance | TextInstance), + ); + } else { + removeChild( + ((hostParent: any): Instance), + (deletedFiber.stateNode: Instance | TextInstance), + ); + } + } } else { - removeChild( - ((currentParent: any): Instance), - (node.stateNode: Instance | TextInstance), + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); } - // Don't visit children because we already visited them. - } else if ( - enableSuspenseServerRenderer && - node.tag === DehydratedFragment - ) { - if (enableSuspenseCallback) { - const hydrationCallbacks = finishedRoot.hydrationCallbacks; - if (hydrationCallbacks !== null) { - const onDeleted = hydrationCallbacks.onDeleted; - if (onDeleted) { - onDeleted((node.stateNode: SuspenseInstance)); + return; + } + case DehydratedFragment: { + if (enableSuspenseServerRenderer) { + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onDeleted = hydrationCallbacks.onDeleted; + if (onDeleted) { + onDeleted((deletedFiber.stateNode: SuspenseInstance)); + } } } - } - // Delete the dehydrated suspense boundary and all of its content. - if (currentParentIsContainer) { - clearSuspenseBoundaryFromContainer( - ((currentParent: any): Container), - (node.stateNode: SuspenseInstance), + // Dehydrated fragments don't have any children + + // Delete the dehydrated suspense boundary and all of its content. + if (supportsMutation) { + if (hostParent !== null) { + if (hostParentIsContainer) { + clearSuspenseBoundaryFromContainer( + ((hostParent: any): Container), + (deletedFiber.stateNode: SuspenseInstance), + ); + } else { + clearSuspenseBoundary( + ((hostParent: any): Instance), + (deletedFiber.stateNode: SuspenseInstance), + ); + } + } + } + } + return; + } + case HostPortal: { + if (supportsMutation) { + // When we go into a portal, it becomes the parent to remove from. + const prevHostParent = hostParent; + const prevHostParentIsContainer = hostParentIsContainer; + hostParent = deletedFiber.stateNode.containerInfo; + hostParentIsContainer = true; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); + hostParent = prevHostParent; + hostParentIsContainer = prevHostParentIsContainer; } else { - clearSuspenseBoundary( - ((currentParent: any): Instance), - (node.stateNode: SuspenseInstance), + emptyPortalContainer(deletedFiber); + + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); } - } else if (node.tag === HostPortal) { - if (node.child !== null) { - // When we go into a portal, it becomes the parent to remove from. - // We will reassign it back when we pop the portal on the way up. - currentParent = node.stateNode.containerInfo; - currentParentIsContainer = true; - // Visit children because portals might contain host components. - node.child.return = node; - node = node.child; - continue; + return; + } + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + if (!offscreenSubtreeWasHidden) { + const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + + let effect = firstEffect; + do { + const {destroy, tag} = effect; + if (destroy !== undefined) { + if ((tag & HookInsertion) !== NoHookEffect) { + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } else if ((tag & HookLayout) !== NoHookEffect) { + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStarted(deletedFiber); + } + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + deletedFiber.mode & ProfileMode + ) { + startLayoutEffectTimer(); + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + recordLayoutEffectDuration(deletedFiber); + } else { + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } + + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } } - } else { - commitUnmount(finishedRoot, node, nearestMountedAncestor); - // Visit children because we may find more host components below. - if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; + + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; + } + case ClassComponent: { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + const instance = deletedFiber.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount( + deletedFiber, + nearestMountedAncestor, + instance, + ); + } } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; } - if (node === current) { + case ScopeComponent: { + if (enableScopeAPI) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); return; } - while (node.sibling === null) { - if (node.return === null || node.return === current) { - return; + case OffscreenComponent: { + // If this offscreen component is hidden, we already unmounted it. Before + // deleting the children, track that it's already unmounted so that we + // don't attempt to unmount the effects again. + // TODO: If the tree is hidden, in most cases we should be able to skip + // over the nested children entirely. An exception is we haven't yet found + // the topmost host node to delete, which we already track on the stack. + // But the other case is portals, which need to be detached no matter how + // deeply they are nested. We should use a subtree flag to track whether a + // subtree includes a nested portal. + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + offscreenSubtreeWasHidden = + prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + break; + } + default: { + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; + } + } +} +function commitSuspenseCallback(finishedWork: Fiber) { + // TODO: Move this to passive phase + const newState: SuspenseState | null = finishedWork.memoizedState; + if (enableSuspenseCallback && newState !== null) { + const suspenseCallback = finishedWork.memoizedProps.suspenseCallback; + if (typeof suspenseCallback === 'function') { + const wakeables: Set | null = (finishedWork.updateQueue: any); + if (wakeables !== null) { + suspenseCallback(new Set(wakeables)); } - node = node.return; - if (node.tag === HostPortal) { - // When we go out of the portal, we need to restore the parent. - // Since we don't keep a stack of them, we will search for it. - currentParentIsValid = false; + } else if (__DEV__) { + if (suspenseCallback !== undefined) { + console.error('Unexpected type for suspenseCallback.'); } } - node.sibling.return = node.return; - node = node.sibling; } } -function commitDeletion( +function commitSuspenseHydrationCallbacks( finishedRoot: FiberRoot, - current: Fiber, - nearestMountedAncestor: Fiber, -): void { - if (supportsMutation) { - // Recursively delete all host nodes from the parent. - // Detach refs and call componentWillUnmount() on the whole subtree. - unmountHostComponents(finishedRoot, current, nearestMountedAncestor); - } else { - // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor); - } - - detachFiberMutation(current); -} - -function commitWork(current: Fiber | null, finishedWork: Fiber): void { - if (!supportsMutation) { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - commitHookEffectListUnmount( - HookInsertion | HookHasEffect, - finishedWork, - finishedWork.return, - ); - commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); - - // Layout effects are destroyed during the mutation phase so that all - // destroy functions for all fibers are called before any create functions. - // This prevents sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - // TODO: Check if we're inside an Offscreen subtree that disappeared - // during this commit. If so, we would have already unmounted its - // layout hooks. (However, since we null out the `destroy` function - // right before calling it, the behavior is already correct, so this - // would mostly be for modeling purposes.) - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } - return; - } - case Profiler: { - return; - } - case SuspenseComponent: { - commitSuspenseCallback(finishedWork); - attachSuspenseRetryListeners(finishedWork); - return; - } - case SuspenseListComponent: { - attachSuspenseRetryListeners(finishedWork); - return; - } - case HostRoot: { - if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } - } - } - break; - } - case OffscreenComponent: - case LegacyHiddenComponent: { - return; - } - } - - commitContainer(finishedWork); - return; - } - - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - commitHookEffectListUnmount( - HookInsertion | HookHasEffect, - finishedWork, - finishedWork.return, - ); - commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); - // Layout effects are destroyed during the mutation phase so that all - // destroy functions for all fibers are called before any create functions. - // This prevents sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } - return; - } - case ClassComponent: { - return; - } - case HostComponent: { - const instance: Instance = finishedWork.stateNode; - if (instance != null) { - // Commit the work prepared earlier. - const newProps = finishedWork.memoizedProps; - // For hydration we reuse the update path but we treat the oldProps - // as the newProps. The updatePayload will contain the real change in - // this case. - const oldProps = current !== null ? current.memoizedProps : newProps; - const type = finishedWork.type; - // TODO: Type the updateQueue to be specific to host components. - const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); - finishedWork.updateQueue = null; - if (updatePayload !== null) { - commitUpdate( - instance, - updatePayload, - type, - oldProps, - newProps, - finishedWork, - ); - } - } - return; - } - case HostText: { - if (finishedWork.stateNode === null) { - throw new Error( - 'This should have a text node initialized. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - } - - const textInstance: TextInstance = finishedWork.stateNode; - const newText: string = finishedWork.memoizedProps; - // For hydration we reuse the update path but we treat the oldProps - // as the newProps. The updatePayload will contain the real change in - // this case. - const oldText: string = - current !== null ? current.memoizedProps : newText; - commitTextUpdate(textInstance, oldText, newText); - return; - } - case HostRoot: { - if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } - } - } - return; - } - case Profiler: { - return; - } - case SuspenseComponent: { - commitSuspenseCallback(finishedWork); - attachSuspenseRetryListeners(finishedWork); - return; - } - case SuspenseListComponent: { - attachSuspenseRetryListeners(finishedWork); - return; - } - case IncompleteClassComponent: { - return; - } - case ScopeComponent: { - if (enableScopeAPI) { - const scopeInstance = finishedWork.stateNode; - prepareScopeUpdate(scopeInstance, finishedWork); - return; - } - break; - } - } - - throw new Error( - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); -} - -function commitSuspenseCallback(finishedWork: Fiber) { - // TODO: Move this to passive phase - const newState: SuspenseState | null = finishedWork.memoizedState; - if (enableSuspenseCallback && newState !== null) { - const suspenseCallback = finishedWork.memoizedProps.suspenseCallback; - if (typeof suspenseCallback === 'function') { - const wakeables: Set | null = (finishedWork.updateQueue: any); - if (wakeables !== null) { - suspenseCallback(new Set(wakeables)); - } - } else if (__DEV__) { - if (suspenseCallback !== undefined) { - console.error('Unexpected type for suspenseCallback.'); - } - } - } -} - -function commitSuspenseHydrationCallbacks( - finishedRoot: FiberRoot, - finishedWork: Fiber, -) { - if (!supportsHydration) { - return; + finishedWork: Fiber, +) { + if (!supportsHydration) { + return; } const newState: SuspenseState | null = finishedWork.memoizedState; if (newState === null) { @@ -2143,77 +1935,51 @@ export function isSuspenseBoundaryBeingHidden( return false; } -function commitResetTextContent(current: Fiber) { - if (!supportsMutation) { - return; - } - resetTextContent(current.stateNode); -} - export function commitMutationEffects( root: FiberRoot, - firstChild: Fiber, + finishedWork: Fiber, committedLanes: Lanes, ) { inProgressLanes = committedLanes; inProgressRoot = root; - nextEffect = firstChild; - commitMutationEffects_begin(root, committedLanes); + setCurrentDebugFiberInDEV(finishedWork); + commitMutationEffectsOnFiber(finishedWork, root, committedLanes); + setCurrentDebugFiberInDEV(finishedWork); inProgressLanes = null; inProgressRoot = null; } -function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) { - while (nextEffect !== null) { - const fiber = nextEffect; - - // TODO: Should wrap this in flags check, too, as optimization - const deletions = fiber.deletions; - if (deletions !== null) { - for (let i = 0; i < deletions.length; i++) { - const childToDelete = deletions[i]; - try { - commitDeletion(root, childToDelete, fiber); - } catch (error) { - reportUncaughtErrorInDEV(error); - captureCommitPhaseError(childToDelete, fiber, error); - } +function recursivelyTraverseMutationEffects( + root: FiberRoot, + parentFiber: Fiber, + lanes: Lanes, +) { + // Deletions effects can be scheduled on any fiber type. They need to happen + // before the children effects hae fired. + const deletions = parentFiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + try { + commitDeletionEffects(root, parentFiber, childToDelete); + } catch (error) { + captureCommitPhaseError(childToDelete, parentFiber, error); } } - - const child = fiber.child; - if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) { - ensureCorrectReturnPointer(child, fiber); - nextEffect = child; - } else { - commitMutationEffects_complete(root, lanes); - } } -} - -function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) { - while (nextEffect !== null) { - const fiber = nextEffect; - setCurrentDebugFiberInDEV(fiber); - try { - commitMutationEffectsOnFiber(fiber, root, lanes); - } catch (error) { - reportUncaughtErrorInDEV(error); - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - const sibling = fiber.sibling; - if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); - nextEffect = sibling; - return; + const prevDebugFiber = getCurrentDebugFiberInDEV(); + if (parentFiber.subtreeFlags & MutationMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + commitMutationEffectsOnFiber(child, root, lanes); + child = child.sibling; } - - nextEffect = fiber.return; } + setCurrentDebugFiberInDEV(prevDebugFiber); } function commitMutationEffectsOnFiber( @@ -2221,50 +1987,264 @@ function commitMutationEffectsOnFiber( root: FiberRoot, lanes: Lanes, ) { - // TODO: The factoring of this phase could probably be improved. Consider - // switching on the type of work before checking the flags. That's what - // we do in all the other phases. I think this one is only different - // because of the shared reconciliation logic below. + const current = finishedWork.alternate; const flags = finishedWork.flags; - if (flags & ContentReset) { - commitResetTextContent(finishedWork); - } + // The effect flag should be checked *after* we refine the type of fiber, + // because the fiber tag is more specific. An exception is any flag related + // to reconcilation, because those can be set on all fiber types. + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - if (flags & Ref) { - const current = finishedWork.alternate; - if (current !== null) { - commitDetachRef(current); + if (flags & Update) { + try { + commitHookEffectListUnmount( + HookInsertion | HookHasEffect, + finishedWork, + finishedWork.return, + ); + commitHookEffectListMount( + HookInsertion | HookHasEffect, + finishedWork, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + recordLayoutEffectDuration(finishedWork); + } else { + try { + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; } - if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away - // from React Flare on www. - if (finishedWork.tag === ScopeComponent) { - commitAttachRef(finishedWork); + case ClassComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + return; + } + case HostComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } } + if (supportsMutation) { + // TODO: ContentReset gets cleared by the children during the commit + // phase. This is a refactor hazard because it means we must read + // flags the flags after `commitReconciliationEffects` has already run; + // the order matters. We should refactor so that ContentReset does not + // rely on mutating the flag during commit. Like by setting a flag + // during the render phase instead. + if (finishedWork.flags & ContentReset) { + const instance: Instance = finishedWork.stateNode; + try { + resetTextContent(instance); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + + if (flags & Update) { + const instance: Instance = finishedWork.stateNode; + if (instance != null) { + // Commit the work prepared earlier. + const newProps = finishedWork.memoizedProps; + // For hydration we reuse the update path but we treat the oldProps + // as the newProps. The updatePayload will contain the real change in + // this case. + const oldProps = + current !== null ? current.memoizedProps : newProps; + const type = finishedWork.type; + // TODO: Type the updateQueue to be specific to host components. + const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); + finishedWork.updateQueue = null; + if (updatePayload !== null) { + try { + commitUpdate( + instance, + updatePayload, + type, + oldProps, + newProps, + finishedWork, + ); + } catch (error) { + captureCommitPhaseError( + finishedWork, + finishedWork.return, + error, + ); + } + } + } + } + } + return; } - } + case HostText: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - if (flags & Visibility) { - switch (finishedWork.tag) { - case SuspenseComponent: { + if (flags & Update) { + if (supportsMutation) { + if (finishedWork.stateNode === null) { + throw new Error( + 'This should have a text node initialized. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + } + + const textInstance: TextInstance = finishedWork.stateNode; + const newText: string = finishedWork.memoizedProps; + // For hydration we reuse the update path but we treat the oldProps + // as the newProps. The updatePayload will contain the real change in + // this case. + const oldText: string = + current !== null ? current.memoizedProps : newText; + + try { + commitTextUpdate(textInstance, oldText, newText); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; + } + case HostRoot: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Update) { + if (supportsMutation && supportsHydration) { + if (current !== null) { + const prevRootState: RootState = current.memoizedState; + if (prevRootState.isDehydrated) { + try { + commitHydratedContainer(root.containerInfo); + } catch (error) { + captureCommitPhaseError( + finishedWork, + finishedWork.return, + error, + ); + } + } + } + } + if (supportsPersistence) { + const containerInfo = root.containerInfo; + const pendingChildren = root.pendingChildren; + try { + replaceContainerChildren(containerInfo, pendingChildren); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; + } + case HostPortal: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Update) { + if (supportsPersistence) { + const portal = finishedWork.stateNode; + const containerInfo = portal.containerInfo; + const pendingChildren = portal.pendingChildren; + try { + replaceContainerChildren(containerInfo, pendingChildren); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; + } + case SuspenseComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Visibility) { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; if (isHidden) { - const current = finishedWork.alternate; const wasHidden = current !== null && current.memoizedState !== null; if (!wasHidden) { // TODO: Move to passive phase markCommitTimeOfFallback(); } } - break; } - case OffscreenComponent: { + if (flags & Update) { + try { + commitSuspenseCallback(finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + attachSuspenseRetryListeners(finishedWork); + } + return; + } + case OffscreenComponent: { + const wasHidden = current !== null && current.memoizedState !== null; + + // Before committing the children, track on the stack whether this + // offscreen subtree was already hidden, so that we don't unmount the + // effects again. + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || wasHidden; + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + + commitReconciliationEffects(finishedWork); + + if (flags & Visibility) { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; - const current = finishedWork.alternate; - const wasHidden = current !== null && current.memoizedState !== null; const offscreenBoundary: Fiber = finishedWork; if (supportsMutation) { @@ -2291,56 +2271,66 @@ function commitMutationEffectsOnFiber( // TODO: Move re-appear call here for symmetry? } } - break; } } + return; } - } + case SuspenseListComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - // The following switch statement is only concerned about placement, - // updates, and deletions. To avoid needing to add a case for every possible - // bitmap value, we remove the secondary effects from the effect tag and - // switch on that value. - const primaryFlags = flags & (Placement | Update | Hydrating); - outer: switch (primaryFlags) { - case Placement: { - commitPlacement(finishedWork); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - // TODO: findDOMNode doesn't rely on this any more but isMounted does - // and isMounted is deprecated anyway so we should be able to kill this. - finishedWork.flags &= ~Placement; - break; + if (flags & Update) { + attachSuspenseRetryListeners(finishedWork); + } + return; } - case PlacementAndUpdate: { - // Placement - commitPlacement(finishedWork); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - finishedWork.flags &= ~Placement; + case ScopeComponent: { + if (enableScopeAPI) { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - // Update - const current = finishedWork.alternate; - commitWork(current, finishedWork); - break; - } - case Hydrating: { - finishedWork.flags &= ~Hydrating; - break; + // TODO: This is a temporary solution that allowed us to transition away + // from React Flare on www. + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(finishedWork, finishedWork.return); + } + safelyAttachRef(finishedWork, finishedWork.return); + } + if (flags & Update) { + const scopeInstance = finishedWork.stateNode; + prepareScopeUpdate(scopeInstance, finishedWork); + } + } + return; } - case HydratingAndUpdate: { - finishedWork.flags &= ~Hydrating; + default: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - // Update - const current = finishedWork.alternate; - commitWork(current, finishedWork); - break; + return; } - case Update: { - const current = finishedWork.alternate; - commitWork(current, finishedWork); - break; + } +} +function commitReconciliationEffects(finishedWork: Fiber) { + // Placement effects (insertions, reorders) can be scheduled on any fiber + // type. They needs to happen after the children effects have fired, but + // before the effects on this fiber have fired. + const flags = finishedWork.flags; + if (flags & Placement) { + try { + commitPlacement(finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); } + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted does + // and isMounted is deprecated anyway so we should be able to kill this. + finishedWork.flags &= ~Placement; + } + if (flags & Hydrating) { + finishedWork.flags &= ~Hydrating; } } @@ -2425,7 +2415,7 @@ function commitLayoutEffects_begin( } if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) { - ensureCorrectReturnPointer(firstChild, fiber); + firstChild.return = fiber; nextEffect = firstChild; } else { commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes); @@ -2446,7 +2436,6 @@ function commitLayoutMountEffects_complete( try { commitLayoutEffectOnFiber(root, current, fiber, committedLanes); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); @@ -2459,7 +2448,7 @@ function commitLayoutMountEffects_complete( const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -2587,7 +2576,6 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) { try { reappearLayoutEffectsOnFiber(fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); @@ -2628,7 +2616,7 @@ function commitPassiveMountEffects_begin( const fiber = nextEffect; const firstChild = fiber.child; if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) { - ensureCorrectReturnPointer(firstChild, fiber); + firstChild.return = fiber; nextEffect = firstChild; } else { commitPassiveMountEffects_complete(subtreeRoot, root, committedLanes); @@ -2649,7 +2637,6 @@ function commitPassiveMountEffects_complete( try { commitPassiveMountOnFiber(root, fiber, committedLanes); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); @@ -2662,7 +2649,7 @@ function commitPassiveMountEffects_complete( const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -2849,7 +2836,7 @@ function commitPassiveUnmountEffects_begin() { } if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) { - ensureCorrectReturnPointer(child, fiber); + child.return = fiber; nextEffect = child; } else { commitPassiveUnmountEffects_complete(); @@ -2868,7 +2855,7 @@ function commitPassiveUnmountEffects_complete() { const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -2923,7 +2910,7 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin( // TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we // do this, still need to handle `deletedTreeCleanUpLevel` correctly.) if (child !== null) { - ensureCorrectReturnPointer(child, fiber); + child.return = fiber; nextEffect = child; } else { commitPassiveUnmountEffectsInsideOfDeletedTree_complete( @@ -2961,7 +2948,7 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( } if (sibling !== null) { - ensureCorrectReturnPointer(sibling, returnFiber); + sibling.return = returnFiber; nextEffect = sibling; return; } @@ -3039,23 +3026,6 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } } -let didWarnWrongReturnPointer = false; -function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { - if (__DEV__) { - if (!didWarnWrongReturnPointer && fiber.return !== expectedReturnFiber) { - didWarnWrongReturnPointer = true; - console.error( - 'Internal React error: Return pointer is inconsistent ' + - 'with parent.', - ); - } - } - - // TODO: Remove this assignment once we're confident that it won't break - // anything, by checking the warning logs for the above invariant - fiber.return = expectedReturnFiber; -} - // TODO: Reuse reappearLayoutEffects traversal here? function invokeLayoutEffectMountInDEV(fiber: Fiber): void { if (__DEV__ && enableStrictEffects) { @@ -3068,7 +3038,6 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { try { commitHookEffectListMount(HookLayout | HookHasEffect, fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3078,7 +3047,6 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { try { instance.componentDidMount(); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3098,7 +3066,6 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void { try { commitHookEffectListMount(HookPassive | HookHasEffect, fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3122,7 +3089,6 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { fiber.return, ); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3153,7 +3119,6 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { fiber.return, ); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } } @@ -3162,10 +3127,7 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { } export { - commitResetTextContent, commitPlacement, - commitDeletion, - commitWork, commitAttachRef, commitDetachRef, invokeLayoutEffectMountInDEV, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 9a8ba5bcd6d5..70b416b3a322 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -68,13 +68,11 @@ import { NoFlags, ContentReset, Placement, - PlacementAndUpdate, ChildDeletion, Snapshot, Update, Ref, Hydrating, - HydratingAndUpdate, Passive, BeforeMutationMask, MutationMask, @@ -86,6 +84,7 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom import { resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, + getCurrentFiber as getCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; import { @@ -182,7 +181,7 @@ let nextEffect: Fiber | null = null; let inProgressLanes: Lanes | null = null; let inProgressRoot: FiberRoot | null = null; -function reportUncaughtErrorInDEV(error) { +export function reportUncaughtErrorInDEV(error: mixed) { // Wrapping each small part of the commit phase into a guarded // callback is a bit too slow (https://github.com/facebook/react/pull/21666). // But we rely on it to surface errors to DEV tools like overlays @@ -223,7 +222,6 @@ function safelyCallCommitHookLayoutEffectListMount( try { commitHookEffectListMount(HookLayout, current); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -237,7 +235,6 @@ function safelyCallComponentWillUnmount( try { callComponentWillUnmountWithTimer(current, instance); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -251,7 +248,6 @@ function safelyCallComponentDidMount( try { instance.componentDidMount(); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -261,7 +257,6 @@ function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { try { commitAttachRef(current); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -287,7 +282,6 @@ function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { retVal = ref(null); } } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } if (__DEV__) { @@ -313,7 +307,6 @@ function safelyCallDestroy( try { destroy(); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(current, nearestMountedAncestor, error); } } @@ -360,7 +353,7 @@ function commitBeforeMutationEffects_begin() { (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null ) { - ensureCorrectReturnPointer(child, fiber); + child.return = fiber; nextEffect = child; } else { commitBeforeMutationEffects_complete(); @@ -375,14 +368,13 @@ function commitBeforeMutationEffects_complete() { try { commitBeforeMutationEffectsOnFiber(fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -1086,21 +1078,28 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) { if (node.tag === HostComponent) { if (hostSubtreeRoot === null) { hostSubtreeRoot = node; - - const instance = node.stateNode; - if (isHidden) { - hideInstance(instance); - } else { - unhideInstance(node.stateNode, node.memoizedProps); + try { + const instance = node.stateNode; + if (isHidden) { + hideInstance(instance); + } else { + unhideInstance(node.stateNode, node.memoizedProps); + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); } } } else if (node.tag === HostText) { if (hostSubtreeRoot === null) { - const instance = node.stateNode; - if (isHidden) { - hideTextInstance(instance); - } else { - unhideTextInstance(instance, node.memoizedProps); + try { + const instance = node.stateNode; + if (isHidden) { + hideTextInstance(instance); + } else { + unhideTextInstance(instance, node.memoizedProps); + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); } } } else if ( @@ -1223,148 +1222,6 @@ function commitDetachRef(current: Fiber) { } } -// User-originating errors (lifecycles and refs) should not interrupt -// deletion, so don't let them throw. Host-originating errors should -// interrupt deletion, so it's okay -function commitUnmount( - finishedRoot: FiberRoot, - current: Fiber, - nearestMountedAncestor: Fiber, -): void { - onCommitUnmount(current); - - switch (current.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); - if (updateQueue !== null) { - const lastEffect = updateQueue.lastEffect; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - - let effect = firstEffect; - do { - const {destroy, tag} = effect; - if (destroy !== undefined) { - if ((tag & HookInsertion) !== NoHookEffect) { - safelyCallDestroy(current, nearestMountedAncestor, destroy); - } else if ((tag & HookLayout) !== NoHookEffect) { - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStarted(current); - } - - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - startLayoutEffectTimer(); - safelyCallDestroy(current, nearestMountedAncestor, destroy); - recordLayoutEffectDuration(current); - } else { - safelyCallDestroy(current, nearestMountedAncestor, destroy); - } - - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStopped(); - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } - } - return; - } - case ClassComponent: { - safelyDetachRef(current, nearestMountedAncestor); - const instance = current.stateNode; - if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount( - current, - nearestMountedAncestor, - instance, - ); - } - return; - } - case HostComponent: { - safelyDetachRef(current, nearestMountedAncestor); - return; - } - case HostPortal: { - // TODO: this is recursive. - // We are also not using this parent because - // the portal will get pushed immediately. - if (supportsMutation) { - unmountHostComponents(finishedRoot, current, nearestMountedAncestor); - } else if (supportsPersistence) { - emptyPortalContainer(current); - } - return; - } - case DehydratedFragment: { - if (enableSuspenseCallback) { - const hydrationCallbacks = finishedRoot.hydrationCallbacks; - if (hydrationCallbacks !== null) { - const onDeleted = hydrationCallbacks.onDeleted; - if (onDeleted) { - onDeleted((current.stateNode: SuspenseInstance)); - } - } - } - return; - } - case ScopeComponent: { - if (enableScopeAPI) { - safelyDetachRef(current, nearestMountedAncestor); - } - return; - } - } -} - -function commitNestedUnmounts( - finishedRoot: FiberRoot, - root: Fiber, - nearestMountedAncestor: Fiber, -): void { - // While we're inside a removed host node we don't want to call - // removeChild on the inner nodes because they're removed by the top - // call anyway. We also want to call componentWillUnmount on all - // composites before this host node is removed from the tree. Therefore - // we do an inner loop while we're still inside the host node. - let node: Fiber = root; - while (true) { - commitUnmount(finishedRoot, node, nearestMountedAncestor); - // Visit children because they may contain more composite or host nodes. - // Skip portals because commitUnmount() currently visits them recursively. - if ( - node.child !== null && - // If we use mutation we drill down into portals using commitUnmount above. - // If we don't use mutation we drill down into portals here instead. - (!supportsMutation || node.tag !== HostPortal) - ) { - node.child.return = node; - node = node.child; - continue; - } - if (node === root) { - return; - } - while (node.sibling === null) { - if (node.return === null || node.return === root) { - return; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - } -} - function detachFiberMutation(fiber: Fiber) { // Cut off the return pointer to disconnect it from the tree. // This enables us to detect and warn against state updates on an unmounted component. @@ -1481,36 +1338,6 @@ function emptyPortalContainer(current: Fiber) { replaceContainerChildren(containerInfo, emptyChildSet); } -function commitContainer(finishedWork: Fiber) { - if (!supportsPersistence) { - return; - } - - switch (finishedWork.tag) { - case ClassComponent: - case HostComponent: - case HostText: { - return; - } - case HostRoot: - case HostPortal: { - const portalOrRoot: { - containerInfo: Container, - pendingChildren: ChildSet, - ... - } = finishedWork.stateNode; - const {containerInfo, pendingChildren} = portalOrRoot; - replaceContainerChildren(containerInfo, pendingChildren); - return; - } - } - - throw new Error( - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); -} - function getHostParentFiber(fiber: Fiber): Fiber { let parent = fiber.return; while (parent !== null) { @@ -1683,388 +1510,353 @@ function insertOrAppendPlacementNode( } } -function unmountHostComponents( - finishedRoot: FiberRoot, - current: Fiber, - nearestMountedAncestor: Fiber, -): void { - // We only have the top Fiber that was deleted but we need to recurse down its - // children to find all the terminal nodes. - let node: Fiber = current; +// These are tracked on the stack as we recursively traverse a +// deleted subtree. +// TODO: Update these during the whole mutation phase, not just during +// a deletion. +let hostParent: Instance | Container | null = null; +let hostParentIsContainer: boolean = false; - // Each iteration, currentParent is populated with node's host parent if not - // currentParentIsValid. - let currentParentIsValid = false; +function commitDeletionEffects( + root: FiberRoot, + returnFiber: Fiber, + deletedFiber: Fiber, +) { + if (supportsMutation) { + // We only have the top Fiber that was deleted but we need to recurse down its + // children to find all the terminal nodes. - // Note: these two variables *must* always be updated together. - let currentParent; - let currentParentIsContainer; - - while (true) { - if (!currentParentIsValid) { - let parent = node.return; - findParent: while (true) { - if (parent === null) { - throw new Error( - 'Expected to find a host parent. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - } + // Recursively delete all host nodes from the parent, detach refs, clean + // up mounted layout effects, and call componentWillUnmount. + + // We only need to remove the topmost host child in each branch. But then we + // still need to keep traversing to unmount effects, refs, and cWU. TODO: We + // could split this into two separate traversals functions, where the second + // one doesn't include any removeChild logic. This is maybe the same + // function as "disappearLayoutEffects" (or whatever that turns into after + // the layout phase is refactored to use recursion). + + // Before starting, find the nearest host parent on the stack so we know + // which instance/container to remove the children from. + // TODO: Instead of searching up the fiber return path on every deletion, we + // can track the nearest host component on the JS stack as we traverse the + // tree during the commit phase. This would make insertions faster, too. + let parent = returnFiber; + findParent: while (parent !== null) { + switch (parent.tag) { + case HostComponent: { + hostParent = parent.stateNode; + hostParentIsContainer = false; + break findParent; + } + case HostRoot: { + hostParent = parent.stateNode.containerInfo; + hostParentIsContainer = true; + break findParent; + } + case HostPortal: { + hostParent = parent.stateNode.containerInfo; + hostParentIsContainer = true; + break findParent; + } + } + parent = parent.return; + } + if (hostParent === null) { + throw new Error( + 'Expected to find a host parent. This error is likely caused by ' + + 'a bug in React. Please file an issue.', + ); + } + commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); + hostParent = null; + hostParentIsContainer = false; + } else { + // Detach refs and call componentWillUnmount() on the whole subtree. + commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); + } + + detachFiberMutation(deletedFiber); +} + +function recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + parent, +) { + // TODO: Use a static flag to skip trees that don't have unmount effects + let child = parent.child; + while (child !== null) { + commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child); + child = child.sibling; + } +} + +function commitDeletionEffectsOnFiber( + finishedRoot: FiberRoot, + nearestMountedAncestor: Fiber, + deletedFiber: Fiber, +) { + onCommitUnmount(deletedFiber); - const parentStateNode = parent.stateNode; - switch (parent.tag) { - case HostComponent: - currentParent = parentStateNode; - currentParentIsContainer = false; - break findParent; - case HostRoot: - currentParent = parentStateNode.containerInfo; - currentParentIsContainer = true; - break findParent; - case HostPortal: - currentParent = parentStateNode.containerInfo; - currentParentIsContainer = true; - break findParent; - } - parent = parent.return; - } - currentParentIsValid = true; - } - - if (node.tag === HostComponent || node.tag === HostText) { - commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor); - // After all the children have unmounted, it is now safe to remove the - // node from the tree. - if (currentParentIsContainer) { - removeChildFromContainer( - ((currentParent: any): Container), - (node.stateNode: Instance | TextInstance), + // The cases in this outer switch modify the stack before they traverse + // into their subtree. There are simpler cases in the inner switch + // that don't modify the stack. + switch (deletedFiber.tag) { + case HostComponent: { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + // Intentional fallthrough to next branch + } + // eslint-disable-next-line-no-fallthrough + case HostText: { + // We only need to remove the nearest host child. Set the host parent + // to `null` on the stack to indicate that nested children don't + // need to be removed. + if (supportsMutation) { + const prevHostParent = hostParent; + const prevHostParentIsContainer = hostParentIsContainer; + hostParent = null; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); + hostParent = prevHostParent; + hostParentIsContainer = prevHostParentIsContainer; + + if (hostParent !== null) { + // Now that all the child effects have unmounted, we can remove the + // node from the tree. + if (hostParentIsContainer) { + removeChildFromContainer( + ((hostParent: any): Container), + (deletedFiber.stateNode: Instance | TextInstance), + ); + } else { + removeChild( + ((hostParent: any): Instance), + (deletedFiber.stateNode: Instance | TextInstance), + ); + } + } } else { - removeChild( - ((currentParent: any): Instance), - (node.stateNode: Instance | TextInstance), + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); } - // Don't visit children because we already visited them. - } else if ( - enableSuspenseServerRenderer && - node.tag === DehydratedFragment - ) { - if (enableSuspenseCallback) { - const hydrationCallbacks = finishedRoot.hydrationCallbacks; - if (hydrationCallbacks !== null) { - const onDeleted = hydrationCallbacks.onDeleted; - if (onDeleted) { - onDeleted((node.stateNode: SuspenseInstance)); + return; + } + case DehydratedFragment: { + if (enableSuspenseServerRenderer) { + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onDeleted = hydrationCallbacks.onDeleted; + if (onDeleted) { + onDeleted((deletedFiber.stateNode: SuspenseInstance)); + } } } - } - // Delete the dehydrated suspense boundary and all of its content. - if (currentParentIsContainer) { - clearSuspenseBoundaryFromContainer( - ((currentParent: any): Container), - (node.stateNode: SuspenseInstance), + // Dehydrated fragments don't have any children + + // Delete the dehydrated suspense boundary and all of its content. + if (supportsMutation) { + if (hostParent !== null) { + if (hostParentIsContainer) { + clearSuspenseBoundaryFromContainer( + ((hostParent: any): Container), + (deletedFiber.stateNode: SuspenseInstance), + ); + } else { + clearSuspenseBoundary( + ((hostParent: any): Instance), + (deletedFiber.stateNode: SuspenseInstance), + ); + } + } + } + } + return; + } + case HostPortal: { + if (supportsMutation) { + // When we go into a portal, it becomes the parent to remove from. + const prevHostParent = hostParent; + const prevHostParentIsContainer = hostParentIsContainer; + hostParent = deletedFiber.stateNode.containerInfo; + hostParentIsContainer = true; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); + hostParent = prevHostParent; + hostParentIsContainer = prevHostParentIsContainer; } else { - clearSuspenseBoundary( - ((currentParent: any): Instance), - (node.stateNode: SuspenseInstance), + emptyPortalContainer(deletedFiber); + + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, ); } - } else if (node.tag === HostPortal) { - if (node.child !== null) { - // When we go into a portal, it becomes the parent to remove from. - // We will reassign it back when we pop the portal on the way up. - currentParent = node.stateNode.containerInfo; - currentParentIsContainer = true; - // Visit children because portals might contain host components. - node.child.return = node; - node = node.child; - continue; + return; + } + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + if (!offscreenSubtreeWasHidden) { + const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + + let effect = firstEffect; + do { + const {destroy, tag} = effect; + if (destroy !== undefined) { + if ((tag & HookInsertion) !== NoHookEffect) { + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } else if ((tag & HookLayout) !== NoHookEffect) { + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStarted(deletedFiber); + } + + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + deletedFiber.mode & ProfileMode + ) { + startLayoutEffectTimer(); + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + recordLayoutEffectDuration(deletedFiber); + } else { + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } + + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } } - } else { - commitUnmount(finishedRoot, node, nearestMountedAncestor); - // Visit children because we may find more host components below. - if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; + + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; + } + case ClassComponent: { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + const instance = deletedFiber.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount( + deletedFiber, + nearestMountedAncestor, + instance, + ); + } } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; } - if (node === current) { + case ScopeComponent: { + if (enableScopeAPI) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); return; } - while (node.sibling === null) { - if (node.return === null || node.return === current) { - return; + case OffscreenComponent: { + // If this offscreen component is hidden, we already unmounted it. Before + // deleting the children, track that it's already unmounted so that we + // don't attempt to unmount the effects again. + // TODO: If the tree is hidden, in most cases we should be able to skip + // over the nested children entirely. An exception is we haven't yet found + // the topmost host node to delete, which we already track on the stack. + // But the other case is portals, which need to be detached no matter how + // deeply they are nested. We should use a subtree flag to track whether a + // subtree includes a nested portal. + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + offscreenSubtreeWasHidden = + prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + break; + } + default: { + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; + } + } +} +function commitSuspenseCallback(finishedWork: Fiber) { + // TODO: Move this to passive phase + const newState: SuspenseState | null = finishedWork.memoizedState; + if (enableSuspenseCallback && newState !== null) { + const suspenseCallback = finishedWork.memoizedProps.suspenseCallback; + if (typeof suspenseCallback === 'function') { + const wakeables: Set | null = (finishedWork.updateQueue: any); + if (wakeables !== null) { + suspenseCallback(new Set(wakeables)); } - node = node.return; - if (node.tag === HostPortal) { - // When we go out of the portal, we need to restore the parent. - // Since we don't keep a stack of them, we will search for it. - currentParentIsValid = false; + } else if (__DEV__) { + if (suspenseCallback !== undefined) { + console.error('Unexpected type for suspenseCallback.'); } } - node.sibling.return = node.return; - node = node.sibling; } } -function commitDeletion( +function commitSuspenseHydrationCallbacks( finishedRoot: FiberRoot, - current: Fiber, - nearestMountedAncestor: Fiber, -): void { - if (supportsMutation) { - // Recursively delete all host nodes from the parent. - // Detach refs and call componentWillUnmount() on the whole subtree. - unmountHostComponents(finishedRoot, current, nearestMountedAncestor); - } else { - // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor); - } - - detachFiberMutation(current); -} - -function commitWork(current: Fiber | null, finishedWork: Fiber): void { - if (!supportsMutation) { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - commitHookEffectListUnmount( - HookInsertion | HookHasEffect, - finishedWork, - finishedWork.return, - ); - commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); - - // Layout effects are destroyed during the mutation phase so that all - // destroy functions for all fibers are called before any create functions. - // This prevents sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - // TODO: Check if we're inside an Offscreen subtree that disappeared - // during this commit. If so, we would have already unmounted its - // layout hooks. (However, since we null out the `destroy` function - // right before calling it, the behavior is already correct, so this - // would mostly be for modeling purposes.) - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } - return; - } - case Profiler: { - return; - } - case SuspenseComponent: { - commitSuspenseCallback(finishedWork); - attachSuspenseRetryListeners(finishedWork); - return; - } - case SuspenseListComponent: { - attachSuspenseRetryListeners(finishedWork); - return; - } - case HostRoot: { - if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } - } - } - break; - } - case OffscreenComponent: - case LegacyHiddenComponent: { - return; - } - } - - commitContainer(finishedWork); - return; - } - - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - commitHookEffectListUnmount( - HookInsertion | HookHasEffect, - finishedWork, - finishedWork.return, - ); - commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); - // Layout effects are destroyed during the mutation phase so that all - // destroy functions for all fibers are called before any create functions. - // This prevents sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - finishedWork, - finishedWork.return, - ); - } - return; - } - case ClassComponent: { - return; - } - case HostComponent: { - const instance: Instance = finishedWork.stateNode; - if (instance != null) { - // Commit the work prepared earlier. - const newProps = finishedWork.memoizedProps; - // For hydration we reuse the update path but we treat the oldProps - // as the newProps. The updatePayload will contain the real change in - // this case. - const oldProps = current !== null ? current.memoizedProps : newProps; - const type = finishedWork.type; - // TODO: Type the updateQueue to be specific to host components. - const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); - finishedWork.updateQueue = null; - if (updatePayload !== null) { - commitUpdate( - instance, - updatePayload, - type, - oldProps, - newProps, - finishedWork, - ); - } - } - return; - } - case HostText: { - if (finishedWork.stateNode === null) { - throw new Error( - 'This should have a text node initialized. This error is likely ' + - 'caused by a bug in React. Please file an issue.', - ); - } - - const textInstance: TextInstance = finishedWork.stateNode; - const newText: string = finishedWork.memoizedProps; - // For hydration we reuse the update path but we treat the oldProps - // as the newProps. The updatePayload will contain the real change in - // this case. - const oldText: string = - current !== null ? current.memoizedProps : newText; - commitTextUpdate(textInstance, oldText, newText); - return; - } - case HostRoot: { - if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } - } - } - return; - } - case Profiler: { - return; - } - case SuspenseComponent: { - commitSuspenseCallback(finishedWork); - attachSuspenseRetryListeners(finishedWork); - return; - } - case SuspenseListComponent: { - attachSuspenseRetryListeners(finishedWork); - return; - } - case IncompleteClassComponent: { - return; - } - case ScopeComponent: { - if (enableScopeAPI) { - const scopeInstance = finishedWork.stateNode; - prepareScopeUpdate(scopeInstance, finishedWork); - return; - } - break; - } - } - - throw new Error( - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); -} - -function commitSuspenseCallback(finishedWork: Fiber) { - // TODO: Move this to passive phase - const newState: SuspenseState | null = finishedWork.memoizedState; - if (enableSuspenseCallback && newState !== null) { - const suspenseCallback = finishedWork.memoizedProps.suspenseCallback; - if (typeof suspenseCallback === 'function') { - const wakeables: Set | null = (finishedWork.updateQueue: any); - if (wakeables !== null) { - suspenseCallback(new Set(wakeables)); - } - } else if (__DEV__) { - if (suspenseCallback !== undefined) { - console.error('Unexpected type for suspenseCallback.'); - } - } - } -} - -function commitSuspenseHydrationCallbacks( - finishedRoot: FiberRoot, - finishedWork: Fiber, -) { - if (!supportsHydration) { - return; + finishedWork: Fiber, +) { + if (!supportsHydration) { + return; } const newState: SuspenseState | null = finishedWork.memoizedState; if (newState === null) { @@ -2143,77 +1935,51 @@ export function isSuspenseBoundaryBeingHidden( return false; } -function commitResetTextContent(current: Fiber) { - if (!supportsMutation) { - return; - } - resetTextContent(current.stateNode); -} - export function commitMutationEffects( root: FiberRoot, - firstChild: Fiber, + finishedWork: Fiber, committedLanes: Lanes, ) { inProgressLanes = committedLanes; inProgressRoot = root; - nextEffect = firstChild; - commitMutationEffects_begin(root, committedLanes); + setCurrentDebugFiberInDEV(finishedWork); + commitMutationEffectsOnFiber(finishedWork, root, committedLanes); + setCurrentDebugFiberInDEV(finishedWork); inProgressLanes = null; inProgressRoot = null; } -function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) { - while (nextEffect !== null) { - const fiber = nextEffect; - - // TODO: Should wrap this in flags check, too, as optimization - const deletions = fiber.deletions; - if (deletions !== null) { - for (let i = 0; i < deletions.length; i++) { - const childToDelete = deletions[i]; - try { - commitDeletion(root, childToDelete, fiber); - } catch (error) { - reportUncaughtErrorInDEV(error); - captureCommitPhaseError(childToDelete, fiber, error); - } +function recursivelyTraverseMutationEffects( + root: FiberRoot, + parentFiber: Fiber, + lanes: Lanes, +) { + // Deletions effects can be scheduled on any fiber type. They need to happen + // before the children effects hae fired. + const deletions = parentFiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + try { + commitDeletionEffects(root, parentFiber, childToDelete); + } catch (error) { + captureCommitPhaseError(childToDelete, parentFiber, error); } } - - const child = fiber.child; - if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) { - ensureCorrectReturnPointer(child, fiber); - nextEffect = child; - } else { - commitMutationEffects_complete(root, lanes); - } } -} - -function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) { - while (nextEffect !== null) { - const fiber = nextEffect; - setCurrentDebugFiberInDEV(fiber); - try { - commitMutationEffectsOnFiber(fiber, root, lanes); - } catch (error) { - reportUncaughtErrorInDEV(error); - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - const sibling = fiber.sibling; - if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); - nextEffect = sibling; - return; + const prevDebugFiber = getCurrentDebugFiberInDEV(); + if (parentFiber.subtreeFlags & MutationMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + commitMutationEffectsOnFiber(child, root, lanes); + child = child.sibling; } - - nextEffect = fiber.return; } + setCurrentDebugFiberInDEV(prevDebugFiber); } function commitMutationEffectsOnFiber( @@ -2221,50 +1987,264 @@ function commitMutationEffectsOnFiber( root: FiberRoot, lanes: Lanes, ) { - // TODO: The factoring of this phase could probably be improved. Consider - // switching on the type of work before checking the flags. That's what - // we do in all the other phases. I think this one is only different - // because of the shared reconciliation logic below. + const current = finishedWork.alternate; const flags = finishedWork.flags; - if (flags & ContentReset) { - commitResetTextContent(finishedWork); - } + // The effect flag should be checked *after* we refine the type of fiber, + // because the fiber tag is more specific. An exception is any flag related + // to reconcilation, because those can be set on all fiber types. + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - if (flags & Ref) { - const current = finishedWork.alternate; - if (current !== null) { - commitDetachRef(current); + if (flags & Update) { + try { + commitHookEffectListUnmount( + HookInsertion | HookHasEffect, + finishedWork, + finishedWork.return, + ); + commitHookEffectListMount( + HookInsertion | HookHasEffect, + finishedWork, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + // Layout effects are destroyed during the mutation phase so that all + // destroy functions for all fibers are called before any create functions. + // This prevents sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + recordLayoutEffectDuration(finishedWork); + } else { + try { + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; } - if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away - // from React Flare on www. - if (finishedWork.tag === ScopeComponent) { - commitAttachRef(finishedWork); + case ClassComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + return; + } + case HostComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } } + if (supportsMutation) { + // TODO: ContentReset gets cleared by the children during the commit + // phase. This is a refactor hazard because it means we must read + // flags the flags after `commitReconciliationEffects` has already run; + // the order matters. We should refactor so that ContentReset does not + // rely on mutating the flag during commit. Like by setting a flag + // during the render phase instead. + if (finishedWork.flags & ContentReset) { + const instance: Instance = finishedWork.stateNode; + try { + resetTextContent(instance); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + + if (flags & Update) { + const instance: Instance = finishedWork.stateNode; + if (instance != null) { + // Commit the work prepared earlier. + const newProps = finishedWork.memoizedProps; + // For hydration we reuse the update path but we treat the oldProps + // as the newProps. The updatePayload will contain the real change in + // this case. + const oldProps = + current !== null ? current.memoizedProps : newProps; + const type = finishedWork.type; + // TODO: Type the updateQueue to be specific to host components. + const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); + finishedWork.updateQueue = null; + if (updatePayload !== null) { + try { + commitUpdate( + instance, + updatePayload, + type, + oldProps, + newProps, + finishedWork, + ); + } catch (error) { + captureCommitPhaseError( + finishedWork, + finishedWork.return, + error, + ); + } + } + } + } + } + return; } - } + case HostText: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - if (flags & Visibility) { - switch (finishedWork.tag) { - case SuspenseComponent: { + if (flags & Update) { + if (supportsMutation) { + if (finishedWork.stateNode === null) { + throw new Error( + 'This should have a text node initialized. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + } + + const textInstance: TextInstance = finishedWork.stateNode; + const newText: string = finishedWork.memoizedProps; + // For hydration we reuse the update path but we treat the oldProps + // as the newProps. The updatePayload will contain the real change in + // this case. + const oldText: string = + current !== null ? current.memoizedProps : newText; + + try { + commitTextUpdate(textInstance, oldText, newText); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; + } + case HostRoot: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Update) { + if (supportsMutation && supportsHydration) { + if (current !== null) { + const prevRootState: RootState = current.memoizedState; + if (prevRootState.isDehydrated) { + try { + commitHydratedContainer(root.containerInfo); + } catch (error) { + captureCommitPhaseError( + finishedWork, + finishedWork.return, + error, + ); + } + } + } + } + if (supportsPersistence) { + const containerInfo = root.containerInfo; + const pendingChildren = root.pendingChildren; + try { + replaceContainerChildren(containerInfo, pendingChildren); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; + } + case HostPortal: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Update) { + if (supportsPersistence) { + const portal = finishedWork.stateNode; + const containerInfo = portal.containerInfo; + const pendingChildren = portal.pendingChildren; + try { + replaceContainerChildren(containerInfo, pendingChildren); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + return; + } + case SuspenseComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Visibility) { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; if (isHidden) { - const current = finishedWork.alternate; const wasHidden = current !== null && current.memoizedState !== null; if (!wasHidden) { // TODO: Move to passive phase markCommitTimeOfFallback(); } } - break; } - case OffscreenComponent: { + if (flags & Update) { + try { + commitSuspenseCallback(finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + attachSuspenseRetryListeners(finishedWork); + } + return; + } + case OffscreenComponent: { + const wasHidden = current !== null && current.memoizedState !== null; + + // Before committing the children, track on the stack whether this + // offscreen subtree was already hidden, so that we don't unmount the + // effects again. + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || wasHidden; + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + + commitReconciliationEffects(finishedWork); + + if (flags & Visibility) { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; - const current = finishedWork.alternate; - const wasHidden = current !== null && current.memoizedState !== null; const offscreenBoundary: Fiber = finishedWork; if (supportsMutation) { @@ -2291,56 +2271,66 @@ function commitMutationEffectsOnFiber( // TODO: Move re-appear call here for symmetry? } } - break; } } + return; } - } + case SuspenseListComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - // The following switch statement is only concerned about placement, - // updates, and deletions. To avoid needing to add a case for every possible - // bitmap value, we remove the secondary effects from the effect tag and - // switch on that value. - const primaryFlags = flags & (Placement | Update | Hydrating); - outer: switch (primaryFlags) { - case Placement: { - commitPlacement(finishedWork); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - // TODO: findDOMNode doesn't rely on this any more but isMounted does - // and isMounted is deprecated anyway so we should be able to kill this. - finishedWork.flags &= ~Placement; - break; + if (flags & Update) { + attachSuspenseRetryListeners(finishedWork); + } + return; } - case PlacementAndUpdate: { - // Placement - commitPlacement(finishedWork); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - finishedWork.flags &= ~Placement; + case ScopeComponent: { + if (enableScopeAPI) { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - // Update - const current = finishedWork.alternate; - commitWork(current, finishedWork); - break; - } - case Hydrating: { - finishedWork.flags &= ~Hydrating; - break; + // TODO: This is a temporary solution that allowed us to transition away + // from React Flare on www. + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(finishedWork, finishedWork.return); + } + safelyAttachRef(finishedWork, finishedWork.return); + } + if (flags & Update) { + const scopeInstance = finishedWork.stateNode; + prepareScopeUpdate(scopeInstance, finishedWork); + } + } + return; } - case HydratingAndUpdate: { - finishedWork.flags &= ~Hydrating; + default: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); - // Update - const current = finishedWork.alternate; - commitWork(current, finishedWork); - break; + return; } - case Update: { - const current = finishedWork.alternate; - commitWork(current, finishedWork); - break; + } +} +function commitReconciliationEffects(finishedWork: Fiber) { + // Placement effects (insertions, reorders) can be scheduled on any fiber + // type. They needs to happen after the children effects have fired, but + // before the effects on this fiber have fired. + const flags = finishedWork.flags; + if (flags & Placement) { + try { + commitPlacement(finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); } + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted does + // and isMounted is deprecated anyway so we should be able to kill this. + finishedWork.flags &= ~Placement; + } + if (flags & Hydrating) { + finishedWork.flags &= ~Hydrating; } } @@ -2425,7 +2415,7 @@ function commitLayoutEffects_begin( } if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) { - ensureCorrectReturnPointer(firstChild, fiber); + firstChild.return = fiber; nextEffect = firstChild; } else { commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes); @@ -2446,7 +2436,6 @@ function commitLayoutMountEffects_complete( try { commitLayoutEffectOnFiber(root, current, fiber, committedLanes); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); @@ -2459,7 +2448,7 @@ function commitLayoutMountEffects_complete( const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -2587,7 +2576,6 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) { try { reappearLayoutEffectsOnFiber(fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); @@ -2628,7 +2616,7 @@ function commitPassiveMountEffects_begin( const fiber = nextEffect; const firstChild = fiber.child; if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) { - ensureCorrectReturnPointer(firstChild, fiber); + firstChild.return = fiber; nextEffect = firstChild; } else { commitPassiveMountEffects_complete(subtreeRoot, root, committedLanes); @@ -2649,7 +2637,6 @@ function commitPassiveMountEffects_complete( try { commitPassiveMountOnFiber(root, fiber, committedLanes); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentDebugFiberInDEV(); @@ -2662,7 +2649,7 @@ function commitPassiveMountEffects_complete( const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -2849,7 +2836,7 @@ function commitPassiveUnmountEffects_begin() { } if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) { - ensureCorrectReturnPointer(child, fiber); + child.return = fiber; nextEffect = child; } else { commitPassiveUnmountEffects_complete(); @@ -2868,7 +2855,7 @@ function commitPassiveUnmountEffects_complete() { const sibling = fiber.sibling; if (sibling !== null) { - ensureCorrectReturnPointer(sibling, fiber.return); + sibling.return = fiber.return; nextEffect = sibling; return; } @@ -2923,7 +2910,7 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin( // TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we // do this, still need to handle `deletedTreeCleanUpLevel` correctly.) if (child !== null) { - ensureCorrectReturnPointer(child, fiber); + child.return = fiber; nextEffect = child; } else { commitPassiveUnmountEffectsInsideOfDeletedTree_complete( @@ -2961,7 +2948,7 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( } if (sibling !== null) { - ensureCorrectReturnPointer(sibling, returnFiber); + sibling.return = returnFiber; nextEffect = sibling; return; } @@ -3039,23 +3026,6 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } } -let didWarnWrongReturnPointer = false; -function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { - if (__DEV__) { - if (!didWarnWrongReturnPointer && fiber.return !== expectedReturnFiber) { - didWarnWrongReturnPointer = true; - console.error( - 'Internal React error: Return pointer is inconsistent ' + - 'with parent.', - ); - } - } - - // TODO: Remove this assignment once we're confident that it won't break - // anything, by checking the warning logs for the above invariant - fiber.return = expectedReturnFiber; -} - // TODO: Reuse reappearLayoutEffects traversal here? function invokeLayoutEffectMountInDEV(fiber: Fiber): void { if (__DEV__ && enableStrictEffects) { @@ -3068,7 +3038,6 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { try { commitHookEffectListMount(HookLayout | HookHasEffect, fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3078,7 +3047,6 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { try { instance.componentDidMount(); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3098,7 +3066,6 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void { try { commitHookEffectListMount(HookPassive | HookHasEffect, fiber); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3122,7 +3089,6 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { fiber.return, ); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } break; @@ -3153,7 +3119,6 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { fiber.return, ); } catch (error) { - reportUncaughtErrorInDEV(error); captureCommitPhaseError(fiber, fiber.return, error); } } @@ -3162,10 +3127,7 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { } export { - commitResetTextContent, commitPlacement, - commitDeletion, - commitWork, commitAttachRef, commitDetachRef, invokeLayoutEffectMountInDEV, diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index 805c4bed918e..d22d9a189b6e 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -18,7 +18,6 @@ export const PerformedWork = /* */ 0b00000000000000000000000001; // You can change the rest (and add more). export const Placement = /* */ 0b00000000000000000000000010; export const Update = /* */ 0b00000000000000000000000100; -export const PlacementAndUpdate = /* */ Placement | Update; export const Deletion = /* */ 0b00000000000000000000001000; export const ChildDeletion = /* */ 0b00000000000000000000010000; export const ContentReset = /* */ 0b00000000000000000000100000; @@ -29,7 +28,6 @@ export const Ref = /* */ 0b00000000000000001000000000; export const Snapshot = /* */ 0b00000000000000010000000000; export const Passive = /* */ 0b00000000000000100000000000; export const Hydrating = /* */ 0b00000000000001000000000000; -export const HydratingAndUpdate = /* */ Hydrating | Update; export const Visibility = /* */ 0b00000000000010000000000000; export const StoreConsistency = /* */ 0b00000000000100000000000000; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index bbd4acdf9e9b..643724724aaa 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -180,6 +180,7 @@ import { invokePassiveEffectMountInDEV, invokeLayoutEffectUnmountInDEV, invokePassiveEffectUnmountInDEV, + reportUncaughtErrorInDEV, } from './ReactFiberCommitWork.new'; import {enqueueUpdate} from './ReactUpdateQueue.new'; import {resetContextDependencies} from './ReactFiberNewContext.new'; @@ -2567,6 +2568,7 @@ export function captureCommitPhaseError( error: mixed, ) { if (__DEV__) { + reportUncaughtErrorInDEV(error); setIsRunningInsertionEffect(false); } if (sourceFiber.tag === HostRoot) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 8e2dd32c9f16..a79e2071b25a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -180,6 +180,7 @@ import { invokePassiveEffectMountInDEV, invokeLayoutEffectUnmountInDEV, invokePassiveEffectUnmountInDEV, + reportUncaughtErrorInDEV, } from './ReactFiberCommitWork.old'; import {enqueueUpdate} from './ReactUpdateQueue.old'; import {resetContextDependencies} from './ReactFiberNewContext.old'; @@ -2567,6 +2568,7 @@ export function captureCommitPhaseError( error: mixed, ) { if (__DEV__) { + reportUncaughtErrorInDEV(error); setIsRunningInsertionEffect(false); } if (sourceFiber.tag === HostRoot) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js index bdda4f8313df..1825d41fd1da 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js @@ -1980,7 +1980,6 @@ describe('ReactSuspenseEffectsSemantics', () => { // Destroy layout and passive effects in the errored tree. 'App destroy layout', - 'ThrowsInWillUnmount componentWillUnmount', 'Text:Fallback destroy layout', 'Text:Outside destroy layout', 'Text:Inside destroy passive', diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js index cb1196baffe6..00c126bb3645 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js @@ -10,18 +10,39 @@ 'use strict'; let React; +let ReactDOM; let ReactDOMClient; +let Scheduler; let act; +let container; describe('ReactSuspenseEffectsSemanticsDOM', () => { beforeEach(() => { jest.resetModules(); React = require('react'); + ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + Scheduler = require('scheduler'); act = require('jest-react').act; + + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); }); + async function fakeImport(result) { + return {default: result}; + } + + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return props.text; + } + it('should not cause a cycle when combined with a render phase update', () => { let scheduleSuspendingUpdate; @@ -63,7 +84,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { } act(() => { - const root = ReactDOMClient.createRoot(document.createElement('div')); + const root = ReactDOMClient.createRoot(container); root.render(); }); @@ -71,4 +92,367 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { scheduleSuspendingUpdate(); }); }); + + it('does not destroy layout effects twice when hidden child is removed', async () => { + function ChildA({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + + function ChildB({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + await LazyChildB; + expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']); + expect(container.innerHTML).toBe('B'); + }); + + it('does not destroy ref cleanup twice when hidden child is removed', async () => { + function ChildA({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + function ChildB({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']); + expect(container.innerHTML).toBe( + 'ALoading...', + ); + + await LazyChildB; + expect(Scheduler).toFlushAndYield(['B', 'Ref mount: B']); + expect(container.innerHTML).toBe('B'); + }); + + it('does not call componentWillUnmount twice when hidden child is removed', async () => { + class ChildA extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + class ChildB extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + await LazyChildB; + expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']); + expect(container.innerHTML).toBe('B'); + }); + + it('does not destroy layout effects twice when parent suspense is removed', async () => { + function ChildA({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + function ChildB({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + // Destroy the whole tree, including the hidden A + ReactDOM.flushSync(() => { + root.render(

Hello

); + }); + expect(Scheduler).toFlushAndYield([]); + expect(container.innerHTML).toBe('

Hello

'); + }); + + it('does not destroy ref cleanup twice when parent suspense is removed', async () => { + function ChildA({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + function ChildB({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']); + expect(container.innerHTML).toBe( + 'ALoading...', + ); + + // Destroy the whole tree, including the hidden A + ReactDOM.flushSync(() => { + root.render(

Hello

); + }); + expect(Scheduler).toFlushAndYield([]); + expect(container.innerHTML).toBe('

Hello

'); + }); + + it('does not call componentWillUnmount twice when parent suspense is removed', async () => { + class ChildA extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + class ChildB extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + // Destroy the whole tree, including the hidden A + ReactDOM.flushSync(() => { + root.render(

Hello

); + }); + expect(Scheduler).toFlushAndYield([]); + expect(container.innerHTML).toBe('

Hello

'); + }); });