From 8122a2f0d5d3f268f9a38aaf6c5c3b182dd4a4c3 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 17 Oct 2022 15:54:42 +0100 Subject: [PATCH 01/17] Do not disconnect Offscreen's ref when detached --- .../src/ReactFiberCommitWork.js | 24 +++++++++++++++---- .../src/__tests__/ReactOffscreen-test.js | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index e394f5671a733..f215e3bf2e49a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2418,13 +2418,29 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { scheduleMicrotask(() => { instance._visibility |= OffscreenDetached; - disappearLayoutEffects(currentOffscreenFiber); - disconnectPassiveEffect(currentOffscreenFiber); + const children = [currentOffscreenFiber.child]; + let node = currentOffscreenFiber.child.sibling; + while (node != null) { + children.push(node); + node = node.sibling; + } + children.forEach((child) => { + disappearLayoutEffects(child); + disconnectPassiveEffect(child); + }); }); } else { instance._visibility |= OffscreenDetached; - disappearLayoutEffects(currentOffscreenFiber); - disconnectPassiveEffect(currentOffscreenFiber); + const children = [currentOffscreenFiber.child]; + let node = currentOffscreenFiber.child.sibling; + while (node != null) { + children.push(node); + node = node.sibling; + } + children.forEach((child) => { + disappearLayoutEffects(child); + disconnectPassiveEffect(child); + }); } } diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index d830265610823..ab9359437f87f 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -1561,6 +1561,7 @@ describe('ReactOffscreen', () => { , ); + expect(offscreenRef.current).not.toBeNull(); }); // @gate enableOffscreen From ad734e7205b8afb07eeb4f2c4d1f6549bc9a9ddf Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Wed, 19 Oct 2022 14:30:34 +0100 Subject: [PATCH 02/17] Add attach method to Offscreen --- .../src/ReactFiberCommitWork.js | 60 +++++++----- .../src/ReactFiberOffscreenComponent.js | 3 +- .../src/__tests__/ReactOffscreen-test.js | 98 ++++++++++++++++++- 3 files changed, 133 insertions(+), 28 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index f215e3bf2e49a..dd626c9920a7d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -17,9 +17,11 @@ import type { } from './ReactFiberHostConfig'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane'; +import {NoTimestamp, SyncLane} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; +import {NoLanes} from './ReactFiberLane'; import type {Wakeable} from 'shared/ReactTypes'; import {isOffscreenManual} from './ReactFiberOffscreenComponent'; import type { @@ -204,7 +206,9 @@ import { import { TransitionRoot, TransitionTracingMarker, -} from './ReactFiberTracingMarkerComponent'; +} from './ReactFiberTracingMarkerComponent.new'; +import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -2414,33 +2418,43 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { ); } - const executionContext = getExecutionContext(); - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - scheduleMicrotask(() => { - instance._visibility |= OffscreenDetached; - const children = [currentOffscreenFiber.child]; - let node = currentOffscreenFiber.child.sibling; + const _detachOffscreen = () => { + instance._visibility |= OffscreenDetached; + let node = currentOffscreenFiber.child?.sibling; while (node != null) { - children.push(node); + disappearLayoutEffects(node); + disconnectPassiveEffect(node); node = node.sibling; } - children.forEach((child) => { - disappearLayoutEffects(child); - disconnectPassiveEffect(child); - }); - }); + }; + + const executionContext = getExecutionContext(); + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { + scheduleMicrotask(_detachOffscreen); } else { - instance._visibility |= OffscreenDetached; - const children = [currentOffscreenFiber.child]; - let node = currentOffscreenFiber.child.sibling; - while (node != null) { - children.push(node); - node = node.sibling; + _detachOffscreen(); + } +} + +export function attachOffscreenInstance(instance: OffscreenInstance): void { + const fiber = instance._current; + if (fiber === null) { + throw new Error( + 'Calling Offscreen.detach before instance handle has been set.', + ); + } + + instance._visibility &= ~OffscreenDetached; + + const executionContext = getExecutionContext(); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } else { + reappearLayoutEffects(root, fiber.alternate, fiber, false); + reconnectPassiveEffects(root, fiber, NoLanes, null, false); } - children.forEach((child) => { - disappearLayoutEffects(child); - disconnectPassiveEffect(child); - }); } } diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index e6445fd66840a..6c68dfac53a48 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -59,8 +59,7 @@ export type OffscreenInstance = { // Represents the current Offscreen fiber _current: Fiber | null, detach: () => void, - - // TODO: attach + attach: () => void, }; export function isOffscreenManual(offscreenFiber: Fiber): boolean { diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index ab9359437f87f..d21f11f0c1926 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -1520,7 +1520,6 @@ describe('ReactOffscreen', () => { ); expect(offscreenRef.current).not.toBeNull(); - expect(offscreenRef.current.detach).not.toBeNull(); // Offscreen is attached by default. State updates from offscreen are **not defered**. await act(async () => { @@ -1538,7 +1537,6 @@ describe('ReactOffscreen', () => { ); }); - // detaching offscreen. offscreenRef.current.detach(); // Offscreen is detached. State updates from offscreen are **defered**. @@ -1561,7 +1559,24 @@ describe('ReactOffscreen', () => { , ); - expect(offscreenRef.current).not.toBeNull(); + + offscreenRef.current.attach(); + + // Offscreen is attached. State updates from offscreen are **not defered**. + await act(async () => { + updateChildState(3); + updateHighPriorityComponentState(3); + expect(Scheduler).toFlushUntilNextPaint([ + 'HighPriorityComponent 3', + 'Child 3', + ]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + }); }); // @gate enableOffscreen @@ -1570,6 +1585,7 @@ describe('ReactOffscreen', () => { let updateHighPriorityComponentState; let offscreenRef; let nextRenderTriggerDetach = false; + let nextRenderTriggerAttach = false; function Child() { const [state, _stateUpdate] = useState(0); @@ -1589,6 +1605,11 @@ describe('ReactOffscreen', () => { updateChildState(state + 1); nextRenderTriggerDetach = false; } + + if (nextRenderTriggerAttach) { + offscreenRef.current.attach(); + nextRenderTriggerAttach = false; + } }); return ( <> @@ -1660,6 +1681,25 @@ describe('ReactOffscreen', () => { , ); + + nextRenderTriggerAttach = true; + + // Offscreen is detached. State updates from offscreen are **defered**. + // Offscreen is attached inside useLayoutEffect; + await act(async () => { + updateChildState(4); + updateHighPriorityComponentState(4); + expect(Scheduler).toFlushUntilNextPaint([ + 'HighPriorityComponent 4', + 'Child 4', + ]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + }); }); }); @@ -1772,5 +1812,57 @@ describe('ReactOffscreen', () => { expect(offscreenRef.current._current === firstFiber).toBeFalsy(); }); + // @gate enableOffscreen + it('does not mount tree until attach is called', async () => { + let offscreenRef; + let spanRef; + + function Child() { + spanRef = useRef(null); + useEffect(() => { + Scheduler.unstable_yieldValue('Mount Child'); + return () => { + Scheduler.unstable_yieldValue('Unmount Child'); + }; + }); + + useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Mount Layout Child'); + return () => { + Scheduler.unstable_yieldValue('Unmount Layout Child'); + }; + }); + + return Child; + } + + function App() { + return ( + (offscreenRef = el)}> + + + ); + } + + const root = ReactNoop.createRoot(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef).not.toBeNull(); + expect(spanRef.current).not.toBeNull(); + expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']); + + offscreenRef.detach(); + + expect(spanRef.current).toBeNull(); + expect(Scheduler).toHaveYielded(['Unmount Layout Child', 'Unmount Child']); + + offscreenRef.attach(); + + expect(spanRef.current).not.toBeNull(); + expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']); + }); // TODO: When attach/detach methods are implemented. Add tests for nested Offscreen case. }); From 98ef81917a23e97e0f999d03c9626eaac24cb27c Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Wed, 19 Oct 2022 18:50:35 +0100 Subject: [PATCH 03/17] Replace fork --- packages/react-reconciler/src/ReactFiber.js | 7 +- .../src/ReactFiberCommitWork.old.js | 4480 +++++++++++++++++ 2 files changed, 4486 insertions(+), 1 deletion(-) create mode 100644 packages/react-reconciler/src/ReactFiberCommitWork.old.js diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 78c0794d432c2..31d7deacb5f30 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -107,7 +107,10 @@ import { REACT_TRACING_MARKER_TYPE, } from 'shared/ReactSymbols'; import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent'; -import {detachOffscreenInstance} from './ReactFiberCommitWork'; +import { + detachOffscreenInstance, + attachOffscreenInstance, +} from './ReactFiberCommitWork'; import {getHostContext} from './ReactFiberHostContext'; export type {Fiber}; @@ -755,6 +758,7 @@ export function createFiberFromOffscreen( _transitions: null, _current: null, detach: () => detachOffscreenInstance(primaryChildInstance), + attach: () => attachOffscreenInstance(primaryChildInstance), }; fiber.stateNode = primaryChildInstance; return fiber; @@ -778,6 +782,7 @@ export function createFiberFromLegacyHidden( _retryCache: null, _current: null, detach: () => detachOffscreenInstance(instance), + attach: () => attachOffscreenInstance(instance), }; fiber.stateNode = instance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js new file mode 100644 index 0000000000000..04f559edb706e --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -0,0 +1,4480 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Instance, + TextInstance, + SuspenseInstance, + Container, + ChildSet, + UpdatePayload, +} from './ReactFiberHostConfig'; +import type {Fiber, FiberRoot} from './ReactInternalTypes'; +import type {Lanes} from './ReactFiberLane.old'; +import {NoTimestamp, SyncLane} from './ReactFiberLane.old'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; +import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; +import {NoLanes} from './ReactFiberLane.old'; +import type {Wakeable} from 'shared/ReactTypes'; +import {isOffscreenManual} from './ReactFiberOffscreenComponent'; +import type { + OffscreenState, + OffscreenInstance, + OffscreenQueue, + OffscreenProps, +} from './ReactFiberOffscreenComponent'; +import type {HookFlags} from './ReactHookEffectTags'; +import type {Cache} from './ReactFiberCacheComponent.old'; +import type {RootState} from './ReactFiberRoot.old'; +import type { + Transition, + TracingMarkerInstance, + TransitionAbort, +} from './ReactFiberTracingMarkerComponent.old'; + +import { + enableCreateEventHandleAPI, + enableProfilerTimer, + enableProfilerCommitHooks, + enableProfilerNestedUpdatePhase, + enableSchedulingProfiler, + enableSuspenseCallback, + enableScopeAPI, + deletedTreeCleanUpLevel, + enableUpdaterTracking, + enableCache, + enableTransitionTracing, + enableUseEventHook, + enableFloat, + enableLegacyHidden, + enableHostSingletons, +} from 'shared/ReactFeatureFlags'; +import { + FunctionComponent, + ForwardRef, + ClassComponent, + HostRoot, + HostComponent, + HostResource, + HostSingleton, + HostText, + HostPortal, + Profiler, + SuspenseComponent, + DehydratedFragment, + IncompleteClassComponent, + MemoComponent, + SimpleMemoComponent, + SuspenseListComponent, + ScopeComponent, + OffscreenComponent, + LegacyHiddenComponent, + CacheComponent, + TracingMarkerComponent, +} from './ReactWorkTags'; +import { + NoFlags, + ContentReset, + Placement, + ChildDeletion, + Snapshot, + Update, + Callback, + Ref, + Hydrating, + Passive, + BeforeMutationMask, + MutationMask, + LayoutMask, + PassiveMask, + Visibility, +} from './ReactFiberFlags'; +import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import { + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, + getCurrentFiber as getCurrentDebugFiberInDEV, +} from './ReactCurrentFiber'; +import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; +import { + isCurrentUpdateNested, + getCommitTime, + recordLayoutEffectDuration, + startLayoutEffectTimer, + recordPassiveEffectDuration, + startPassiveEffectTimer, +} from './ReactProfilerTimer.old'; +import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; +import { + deferHiddenCallbacks, + commitHiddenCallbacks, + commitCallbacks, +} from './ReactFiberClassUpdateQueue.old'; +import { + getPublicInstance, + supportsMutation, + supportsPersistence, + supportsHydration, + supportsResources, + supportsSingletons, + commitMount, + commitUpdate, + resetTextContent, + commitTextUpdate, + appendChild, + appendChildToContainer, + insertBefore, + insertInContainerBefore, + removeChild, + removeChildFromContainer, + clearSuspenseBoundary, + clearSuspenseBoundaryFromContainer, + replaceContainerChildren, + createContainerChildSet, + hideInstance, + hideTextInstance, + unhideInstance, + unhideTextInstance, + commitHydratedContainer, + commitHydratedSuspenseInstance, + clearContainer, + prepareScopeUpdate, + prepareForCommit, + beforeActiveInstanceBlur, + detachDeletedInstance, + acquireResource, + releaseResource, + clearSingleton, + acquireSingletonInstance, + releaseSingletonInstance, + scheduleMicrotask, +} from './ReactFiberHostConfig'; +import { + captureCommitPhaseError, + resolveRetryWakeable, + markCommitTimeOfFallback, + enqueuePendingPassiveProfilerEffect, + restorePendingUpdaters, + addTransitionStartCallbackToPendingTransition, + addTransitionProgressCallbackToPendingTransition, + addTransitionCompleteCallbackToPendingTransition, + addMarkerProgressCallbackToPendingTransition, + addMarkerIncompleteCallbackToPendingTransition, + addMarkerCompleteCallbackToPendingTransition, + setIsRunningInsertionEffect, + getExecutionContext, + CommitContext, + RenderContext, + NoContext, +} from './ReactFiberWorkLoop.old'; +import { + NoFlags as NoHookEffect, + HasEffect as HookHasEffect, + Layout as HookLayout, + Insertion as HookInsertion, + Passive as HookPassive, +} from './ReactHookEffectTags'; +import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old'; +import {doesFiberContain} from './ReactFiberTreeReflection'; +import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils'; +import { + isDevToolsPresent, + markComponentPassiveEffectMountStarted, + markComponentPassiveEffectMountStopped, + markComponentPassiveEffectUnmountStarted, + markComponentPassiveEffectUnmountStopped, + markComponentLayoutEffectMountStarted, + markComponentLayoutEffectMountStopped, + markComponentLayoutEffectUnmountStarted, + markComponentLayoutEffectUnmountStopped, + onCommitUnmount, +} from './ReactFiberDevToolsHook.old'; +import {releaseCache, retainCache} from './ReactFiberCacheComponent.old'; +import {clearTransitionsForLanes} from './ReactFiberLane.old'; +import { + OffscreenVisible, + OffscreenDetached, + OffscreenPassiveEffectsConnected, +} from './ReactFiberOffscreenComponent'; +import { + TransitionRoot, + TransitionTracingMarker, +} from './ReactFiberTracingMarkerComponent.old'; +import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop.old'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; + +let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; +if (__DEV__) { + didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); +} + +// Used during the commit phase to track the state of the Offscreen component stack. +// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor. +let offscreenSubtreeIsHidden: boolean = false; +let offscreenSubtreeWasHidden: boolean = false; + +const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; + +let nextEffect: Fiber | null = null; + +// Used for Profiling builds to track updaters. +let inProgressLanes: Lanes | null = null; +let inProgressRoot: FiberRoot | null = null; + +function shouldProfile(current: Fiber): boolean { + return ( + enableProfilerTimer && + enableProfilerCommitHooks && + (current.mode & ProfileMode) !== NoMode && + (getExecutionContext() & CommitContext) !== NoContext + ); +} + +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 + // (https://github.com/facebook/react/issues/21712). + // As a compromise, rethrow only caught errors in a guard. + if (__DEV__) { + invokeGuardedCallback(null, () => { + throw error; + }); + clearCaughtError(); + } +} + +const callComponentWillUnmountWithTimer = function(current, instance) { + instance.props = current.memoizedProps; + instance.state = current.memoizedState; + if (shouldProfile(current)) { + try { + startLayoutEffectTimer(); + instance.componentWillUnmount(); + } finally { + recordLayoutEffectDuration(current); + } + } else { + instance.componentWillUnmount(); + } +}; + +// Capture errors so they don't interrupt unmounting. +function safelyCallComponentWillUnmount( + current: Fiber, + nearestMountedAncestor: Fiber | null, + instance: any, +) { + try { + callComponentWillUnmountWithTimer(current, instance); + } catch (error) { + captureCommitPhaseError(current, nearestMountedAncestor, error); + } +} + +// Capture errors so they don't interrupt mounting. +function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { + try { + commitAttachRef(current); + } catch (error) { + captureCommitPhaseError(current, nearestMountedAncestor, error); + } +} + +function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { + const ref = current.ref; + const refCleanup = current.refCleanup; + + if (ref !== null) { + if (typeof refCleanup === 'function') { + try { + if (shouldProfile(current)) { + try { + startLayoutEffectTimer(); + refCleanup(); + } finally { + recordLayoutEffectDuration(current); + } + } else { + refCleanup(); + } + } catch (error) { + captureCommitPhaseError(current, nearestMountedAncestor, error); + } finally { + // `refCleanup` has been called. Nullify all references to it to prevent double invocation. + current.refCleanup = null; + const finishedWork = current.alternate; + if (finishedWork != null) { + finishedWork.refCleanup = null; + } + } + } else if (typeof ref === 'function') { + let retVal; + try { + if (shouldProfile(current)) { + try { + startLayoutEffectTimer(); + retVal = ref(null); + } finally { + recordLayoutEffectDuration(current); + } + } else { + retVal = ref(null); + } + } catch (error) { + captureCommitPhaseError(current, nearestMountedAncestor, error); + } + if (__DEV__) { + if (typeof retVal === 'function') { + console.error( + 'Unexpected return value from a callback ref in %s. ' + + 'A callback ref should not return a function.', + getComponentNameFromFiber(current), + ); + } + } + } else { + // $FlowFixMe unable to narrow type to RefObject + ref.current = null; + } + } +} + +function safelyCallDestroy( + current: Fiber, + nearestMountedAncestor: Fiber | null, + destroy: () => void, +) { + try { + destroy(); + } catch (error) { + captureCommitPhaseError(current, nearestMountedAncestor, error); + } +} + +let focusedInstanceHandle: null | Fiber = null; +let shouldFireAfterActiveInstanceBlur: boolean = false; + +export function commitBeforeMutationEffects( + root: FiberRoot, + firstChild: Fiber, +): boolean { + focusedInstanceHandle = prepareForCommit(root.containerInfo); + + nextEffect = firstChild; + commitBeforeMutationEffects_begin(); + + // We no longer need to track the active instance fiber + const shouldFire = shouldFireAfterActiveInstanceBlur; + shouldFireAfterActiveInstanceBlur = false; + focusedInstanceHandle = null; + + return shouldFire; +} + +function commitBeforeMutationEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + + // This phase is only used for beforeActiveInstanceBlur. + // Let's skip the whole loop if it's off. + if (enableCreateEventHandleAPI) { + // 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 deletion = deletions[i]; + commitBeforeMutationEffectsDeletion(deletion); + } + } + } + + const child = fiber.child; + if ( + (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && + child !== null + ) { + child.return = fiber; + nextEffect = child; + } else { + commitBeforeMutationEffects_complete(); + } + } +} + +function commitBeforeMutationEffects_complete() { + while (nextEffect !== null) { + const fiber = nextEffect; + setCurrentDebugFiberInDEV(fiber); + try { + commitBeforeMutationEffectsOnFiber(fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { + const current = finishedWork.alternate; + const flags = finishedWork.flags; + + if (enableCreateEventHandleAPI) { + if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { + // Check to see if the focused element was inside of a hidden (Suspense) subtree. + // TODO: Move this out of the hot path using a dedicated effect tag. + if ( + finishedWork.tag === SuspenseComponent && + isSuspenseBoundaryBeingHidden(current, finishedWork) && + // $FlowFixMe[incompatible-call] found when upgrading Flow + doesFiberContain(finishedWork, focusedInstanceHandle) + ) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(finishedWork); + } + } + } + + if ((flags & Snapshot) !== NoFlags) { + setCurrentDebugFiberInDEV(finishedWork); + } + + switch (finishedWork.tag) { + case FunctionComponent: { + if (enableUseEventHook) { + if ((flags & Update) !== NoFlags) { + commitUseEventMount(finishedWork); + } + } + break; + } + case ForwardRef: + case SimpleMemoComponent: { + break; + } + case ClassComponent: { + if ((flags & Snapshot) !== NoFlags) { + if (current !== null) { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + const instance = finishedWork.stateNode; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + } + } + const snapshot = instance.getSnapshotBeforeUpdate( + finishedWork.elementType === finishedWork.type + ? prevProps + : resolveDefaultProps(finishedWork.type, prevProps), + prevState, + ); + if (__DEV__) { + const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); + if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { + didWarnSet.add(finishedWork.type); + console.error( + '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + + 'must be returned. You have returned undefined.', + getComponentNameFromFiber(finishedWork), + ); + } + } + instance.__reactInternalSnapshotBeforeUpdate = snapshot; + } + } + break; + } + case HostRoot: { + if ((flags & Snapshot) !== NoFlags) { + if (supportsMutation) { + const root = finishedWork.stateNode; + clearContainer(root.containerInfo); + } + } + break; + } + case HostComponent: + case HostResource: + case HostSingleton: + case HostText: + case HostPortal: + case IncompleteClassComponent: + // Nothing to do for these component types + break; + default: { + if ((flags & Snapshot) !== NoFlags) { + 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.', + ); + } + } + } + + if ((flags & Snapshot) !== NoFlags) { + resetCurrentDebugFiberInDEV(); + } +} + +function commitBeforeMutationEffectsDeletion(deletion: Fiber) { + if (enableCreateEventHandleAPI) { + // TODO (effects) It would be nice to avoid calling doesFiberContain() + // Maybe we can repurpose one of the subtreeFlags positions for this instead? + // Use it to store which part of the tree the focused instance is in? + // This assumes we can safely determine that instance during the "render" phase. + if (doesFiberContain(deletion, ((focusedInstanceHandle: any): Fiber))) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(deletion); + } + } +} + +function commitHookEffectListUnmount( + flags: HookFlags, + finishedWork: Fiber, + nearestMountedAncestor: Fiber | null, +) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + if ((effect.tag & flags) === flags) { + // Unmount + const destroy = effect.destroy; + effect.destroy = undefined; + if (destroy !== undefined) { + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectUnmountStarted(finishedWork); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectUnmountStarted(finishedWork); + } + } + + if (__DEV__) { + if ((flags & HookInsertion) !== NoHookEffect) { + setIsRunningInsertionEffect(true); + } + } + safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); + if (__DEV__) { + if ((flags & HookInsertion) !== NoHookEffect) { + setIsRunningInsertionEffect(false); + } + } + + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectUnmountStopped(); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectUnmountStopped(); + } + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } +} + +function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + if ((effect.tag & flags) === flags) { + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectMountStarted(finishedWork); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectMountStarted(finishedWork); + } + } + + // Mount + const create = effect.create; + if (__DEV__) { + if ((flags & HookInsertion) !== NoHookEffect) { + setIsRunningInsertionEffect(true); + } + } + effect.destroy = create(); + if (__DEV__) { + if ((flags & HookInsertion) !== NoHookEffect) { + setIsRunningInsertionEffect(false); + } + } + + if (enableSchedulingProfiler) { + if ((flags & HookPassive) !== NoHookEffect) { + markComponentPassiveEffectMountStopped(); + } else if ((flags & HookLayout) !== NoHookEffect) { + markComponentLayoutEffectMountStopped(); + } + } + + if (__DEV__) { + const destroy = effect.destroy; + if (destroy !== undefined && typeof destroy !== 'function') { + let hookName; + if ((effect.tag & HookLayout) !== NoFlags) { + hookName = 'useLayoutEffect'; + } else if ((effect.tag & HookInsertion) !== NoFlags) { + hookName = 'useInsertionEffect'; + } else { + hookName = 'useEffect'; + } + let addendum; + if (destroy === null) { + addendum = + ' You returned null. If your effect does not require clean ' + + 'up, return undefined (or nothing).'; + } else if (typeof destroy.then === 'function') { + addendum = + '\n\nIt looks like you wrote ' + + hookName + + '(async () => ...) or returned a Promise. ' + + 'Instead, write the async function inside your effect ' + + 'and call it immediately:\n\n' + + hookName + + '(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'; + } else { + addendum = ' You returned: ' + destroy; + } + console.error( + '%s must not return anything besides a function, ' + + 'which is used for clean-up.%s', + hookName, + addendum, + ); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } +} + +function commitUseEventMount(finishedWork: Fiber) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const eventPayloads = updateQueue !== null ? updateQueue.events : null; + if (eventPayloads !== null) { + for (let ii = 0; ii < eventPayloads.length; ii++) { + const {ref, nextImpl} = eventPayloads[ii]; + ref.impl = nextImpl; + } + } +} + +export function commitPassiveEffectDurations( + finishedRoot: FiberRoot, + finishedWork: Fiber, +): void { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + getExecutionContext() & CommitContext + ) { + // Only Profilers with work in their subtree will have an Update effect scheduled. + if ((finishedWork.flags & Update) !== NoFlags) { + switch (finishedWork.tag) { + case Profiler: { + const {passiveEffectDuration} = finishedWork.stateNode; + const {id, onPostCommit} = finishedWork.memoizedProps; + + // This value will still reflect the previous commit phase. + // It does not get reset until the start of the next commit phase. + const commitTime = getCommitTime(); + + let phase = finishedWork.alternate === null ? 'mount' : 'update'; + if (enableProfilerNestedUpdatePhase) { + if (isCurrentUpdateNested()) { + phase = 'nested-update'; + } + } + + if (typeof onPostCommit === 'function') { + onPostCommit(id, phase, passiveEffectDuration, commitTime); + } + + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + let parentFiber = finishedWork.return; + outer: while (parentFiber !== null) { + switch (parentFiber.tag) { + case HostRoot: + const root = parentFiber.stateNode; + root.passiveEffectDuration += passiveEffectDuration; + break outer; + case Profiler: + const parentStateNode = parentFiber.stateNode; + parentStateNode.passiveEffectDuration += passiveEffectDuration; + break outer; + } + parentFiber = parentFiber.return; + } + break; + } + default: + break; + } + } + } +} + +function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) { + // At this point layout effects have already been destroyed (during mutation phase). + // This is done to prevent sibling component effects from interfering with each other, + // e.g. a destroy function in one component should never override a ref set + // by a create function in another component during the same commit. + if (shouldProfile(finishedWork)) { + try { + startLayoutEffectTimer(); + commitHookEffectListMount(hookFlags, finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + recordLayoutEffectDuration(finishedWork); + } else { + try { + commitHookEffectListMount(hookFlags, finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } +} + +function commitClassLayoutLifecycles( + finishedWork: Fiber, + current: Fiber | null, +) { + const instance = finishedWork.stateNode; + if (current === null) { + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidMount. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidMount. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + } + } + if (shouldProfile(finishedWork)) { + try { + startLayoutEffectTimer(); + instance.componentDidMount(); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + recordLayoutEffectDuration(finishedWork); + } else { + try { + instance.componentDidMount(); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } else { + const prevProps = + finishedWork.elementType === finishedWork.type + ? current.memoizedProps + : resolveDefaultProps(finishedWork.type, current.memoizedProps); + const prevState = current.memoizedState; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + } + } + if (shouldProfile(finishedWork)) { + try { + startLayoutEffectTimer(); + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + recordLayoutEffectDuration(finishedWork); + } else { + try { + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } +} + +function commitClassCallbacks(finishedWork: Fiber) { + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + const instance = finishedWork.stateNode; + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'processing the update queue. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'processing the update queue. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentNameFromFiber(finishedWork) || 'instance', + ); + } + } + } + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + try { + commitCallbacks(updateQueue, instance); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } +} + +function commitHostComponentMount(finishedWork: Fiber) { + const type = finishedWork.type; + const props = finishedWork.memoizedProps; + const instance: Instance = finishedWork.stateNode; + try { + commitMount(instance, type, props, finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } +} + +function commitProfilerUpdate(finishedWork: Fiber, current: Fiber | null) { + if (enableProfilerTimer && getExecutionContext() & CommitContext) { + try { + const {onCommit, onRender} = finishedWork.memoizedProps; + const {effectDuration} = finishedWork.stateNode; + + const commitTime = getCommitTime(); + + let phase = current === null ? 'mount' : 'update'; + if (enableProfilerNestedUpdatePhase) { + if (isCurrentUpdateNested()) { + phase = 'nested-update'; + } + } + + if (typeof onRender === 'function') { + onRender( + finishedWork.memoizedProps.id, + phase, + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); + } + + if (enableProfilerCommitHooks) { + if (typeof onCommit === 'function') { + onCommit( + finishedWork.memoizedProps.id, + phase, + effectDuration, + commitTime, + ); + } + + // Schedule a passive effect for this Profiler to call onPostCommit hooks. + // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, + // because the effect is also where times bubble to parent Profilers. + enqueuePendingPassiveProfilerEffect(finishedWork); + + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + let parentFiber = finishedWork.return; + outer: while (parentFiber !== null) { + switch (parentFiber.tag) { + case HostRoot: + const root = parentFiber.stateNode; + root.effectDuration += effectDuration; + break outer; + case Profiler: + const parentStateNode = parentFiber.stateNode; + parentStateNode.effectDuration += effectDuration; + break outer; + } + parentFiber = parentFiber.return; + } + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } +} + +function commitLayoutEffectOnFiber( + finishedRoot: FiberRoot, + current: Fiber | null, + finishedWork: Fiber, + committedLanes: Lanes, +): void { + // When updating this function, also update reappearLayoutEffects, which does + // most of the same things when an offscreen tree goes from hidden -> visible. + const flags = finishedWork.flags; + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + if (flags & Update) { + commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect); + } + break; + } + case ClassComponent: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + if (flags & Update) { + commitClassLayoutLifecycles(finishedWork, current); + } + + if (flags & Callback) { + commitClassCallbacks(finishedWork); + } + + if (flags & Ref) { + safelyAttachRef(finishedWork, finishedWork.return); + } + break; + } + case HostRoot: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + if (flags & Callback) { + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostSingleton: + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } + } + try { + commitCallbacks(updateQueue, instance); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + break; + } + case HostResource: { + if (enableFloat && supportsResources) { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + + if (flags & Ref) { + safelyAttachRef(finishedWork, finishedWork.return); + } + break; + } + } + // eslint-disable-next-line-no-fallthrough + case HostSingleton: + case HostComponent: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + + // Renderers may schedule work to be done after host components are mounted + // (eg DOM renderer may schedule auto-focus for inputs and form controls). + // These effects should only be committed when components are first mounted, + // aka when there is no current/alternate. + if (current === null && flags & Update) { + commitHostComponentMount(finishedWork); + } + + if (flags & Ref) { + safelyAttachRef(finishedWork, finishedWork.return); + } + break; + } + case Profiler: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + // TODO: Should this fire inside an offscreen tree? Or should it wait to + // fire when the tree becomes visible again. + if (flags & Update) { + commitProfilerUpdate(finishedWork, current); + } + break; + } + case SuspenseComponent: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + if (flags & Update) { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + } + break; + } + case OffscreenComponent: { + const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode; + if (isModernRoot) { + const isHidden = finishedWork.memoizedState !== null; + const newOffscreenSubtreeIsHidden = + isHidden || offscreenSubtreeIsHidden; + if (newOffscreenSubtreeIsHidden) { + // The Offscreen tree is hidden. Skip over its layout effects. + } else { + // The Offscreen tree is visible. + + const wasHidden = current !== null && current.memoizedState !== null; + const newOffscreenSubtreeWasHidden = + wasHidden || offscreenSubtreeWasHidden; + const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden; + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden; + offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden; + + if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) { + // This is the root of a reappearing boundary. As we continue + // traversing the layout effects, we must also re-mount layout + // effects that were unmounted when the Offscreen subtree was + // hidden. So this is a superset of the normal commitLayoutEffects. + const includeWorkInProgressEffects = + (finishedWork.subtreeFlags & LayoutMask) !== NoFlags; + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + } else { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + } + offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden; + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + } + } else { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + } + if (flags & Ref) { + const props: OffscreenProps = finishedWork.memoizedProps; + if (props.mode === 'manual') { + safelyAttachRef(finishedWork, finishedWork.return); + } else { + safelyDetachRef(finishedWork, finishedWork.return); + } + } + break; + } + default: { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + break; + } + } +} + +function abortRootTransitions( + root: FiberRoot, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + const rootTransitions = root.incompleteTransitions; + deletedTransitions.forEach(transition => { + if (rootTransitions.has(transition)) { + const transitionInstance: TracingMarkerInstance = (rootTransitions.get( + transition, + ): any); + if (transitionInstance.aborts === null) { + transitionInstance.aborts = []; + } + transitionInstance.aborts.push(abort); + + if (deletedOffscreenInstance !== null) { + if ( + transitionInstance.pendingBoundaries !== null && + transitionInstance.pendingBoundaries.has(deletedOffscreenInstance) + ) { + // $FlowFixMe[incompatible-use] found when upgrading Flow + transitionInstance.pendingBoundaries.delete( + deletedOffscreenInstance, + ); + } + } + } + }); + } +} + +function abortTracingMarkerTransitions( + abortedFiber: Fiber, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + const markerInstance: TracingMarkerInstance = abortedFiber.stateNode; + const markerTransitions = markerInstance.transitions; + const pendingBoundaries = markerInstance.pendingBoundaries; + if (markerTransitions !== null) { + // TODO: Refactor this code. Is there a way to move this code to + // the deletions phase instead of calculating it here while making sure + // complete is called appropriately? + deletedTransitions.forEach(transition => { + // If one of the transitions on the tracing marker is a transition + // that was in an aborted subtree, we will abort that tracing marker + if ( + abortedFiber !== null && + markerTransitions.has(transition) && + (markerInstance.aborts === null || + !markerInstance.aborts.includes(abort)) + ) { + if (markerInstance.transitions !== null) { + if (markerInstance.aborts === null) { + markerInstance.aborts = [abort]; + addMarkerIncompleteCallbackToPendingTransition( + abortedFiber.memoizedProps.name, + markerInstance.transitions, + markerInstance.aborts, + ); + } else { + markerInstance.aborts.push(abort); + } + + // We only want to call onTransitionProgress when the marker hasn't been + // deleted + if ( + deletedOffscreenInstance !== null && + !isInDeletedTree && + pendingBoundaries !== null && + pendingBoundaries.has(deletedOffscreenInstance) + ) { + pendingBoundaries.delete(deletedOffscreenInstance); + + addMarkerProgressCallbackToPendingTransition( + abortedFiber.memoizedProps.name, + deletedTransitions, + pendingBoundaries, + ); + } + } + } + }); + } + } +} + +function abortParentMarkerTransitionsForDeletedFiber( + abortedFiber: Fiber, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + // Find all pending markers that are waiting on child suspense boundaries in the + // aborted subtree and cancels them + let fiber: null | Fiber = abortedFiber; + while (fiber !== null) { + switch (fiber.tag) { + case TracingMarkerComponent: + abortTracingMarkerTransitions( + fiber, + abort, + deletedTransitions, + deletedOffscreenInstance, + isInDeletedTree, + ); + break; + case HostRoot: + const root = fiber.stateNode; + abortRootTransitions( + root, + abort, + deletedTransitions, + deletedOffscreenInstance, + isInDeletedTree, + ); + + break; + default: + break; + } + + fiber = fiber.return; + } + } +} + +function commitTransitionProgress(offscreenFiber: Fiber) { + if (enableTransitionTracing) { + // This function adds suspense boundaries to the root + // or tracing marker's pendingBoundaries map. + // When a suspense boundary goes from a resolved to a fallback + // state we add the boundary to the map, and when it goes from + // a fallback to a resolved state, we remove the boundary from + // the map. + + // We use stateNode on the Offscreen component as a stable object + // that doesnt change from render to render. This way we can + // distinguish between different Offscreen instances (vs. the same + // Offscreen instance with different fibers) + const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode; + + let prevState: SuspenseState | null = null; + const previousFiber = offscreenFiber.alternate; + if (previousFiber !== null && previousFiber.memoizedState !== null) { + prevState = previousFiber.memoizedState; + } + const nextState: SuspenseState | null = offscreenFiber.memoizedState; + + const wasHidden = prevState !== null; + const isHidden = nextState !== null; + + const pendingMarkers = offscreenInstance._pendingMarkers; + // If there is a name on the suspense boundary, store that in + // the pending boundaries. + let name = null; + const parent = offscreenFiber.return; + if ( + parent !== null && + parent.tag === SuspenseComponent && + parent.memoizedProps.unstable_name + ) { + name = parent.memoizedProps.unstable_name; + } + + if (!wasHidden && isHidden) { + // The suspense boundaries was just hidden. Add the boundary + // to the pending boundary set if it's there + if (pendingMarkers !== null) { + pendingMarkers.forEach(markerInstance => { + const pendingBoundaries = markerInstance.pendingBoundaries; + const transitions = markerInstance.transitions; + const markerName = markerInstance.name; + if ( + pendingBoundaries !== null && + !pendingBoundaries.has(offscreenInstance) + ) { + pendingBoundaries.set(offscreenInstance, { + name, + }); + if (transitions !== null) { + if ( + markerInstance.tag === TransitionTracingMarker && + markerName !== null + ) { + addMarkerProgressCallbackToPendingTransition( + markerName, + transitions, + pendingBoundaries, + ); + } else if (markerInstance.tag === TransitionRoot) { + transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition( + transition, + pendingBoundaries, + ); + }); + } + } + } + }); + } + } else if (wasHidden && !isHidden) { + // The suspense boundary went from hidden to visible. Remove + // the boundary from the pending suspense boundaries set + // if it's there + if (pendingMarkers !== null) { + pendingMarkers.forEach(markerInstance => { + const pendingBoundaries = markerInstance.pendingBoundaries; + const transitions = markerInstance.transitions; + const markerName = markerInstance.name; + if ( + pendingBoundaries !== null && + pendingBoundaries.has(offscreenInstance) + ) { + pendingBoundaries.delete(offscreenInstance); + if (transitions !== null) { + if ( + markerInstance.tag === TransitionTracingMarker && + markerName !== null + ) { + addMarkerProgressCallbackToPendingTransition( + markerName, + transitions, + pendingBoundaries, + ); + + // If there are no more unresolved suspense boundaries, the interaction + // is considered finished + if (pendingBoundaries.size === 0) { + if (markerInstance.aborts === null) { + addMarkerCompleteCallbackToPendingTransition( + markerName, + transitions, + ); + } + markerInstance.transitions = null; + markerInstance.pendingBoundaries = null; + markerInstance.aborts = null; + } + } else if (markerInstance.tag === TransitionRoot) { + transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition( + transition, + pendingBoundaries, + ); + }); + } + } + } + }); + } + } + } +} + +function hideOrUnhideAllChildren(finishedWork, isHidden) { + // Only hide or unhide the top-most host nodes. + let hostSubtreeRoot = null; + + if (supportsMutation) { + // We only have the top Fiber that was inserted but we need to recurse down its + // children to find all the terminal nodes. + let node: Fiber = finishedWork; + while (true) { + if ( + node.tag === HostComponent || + (enableFloat && supportsResources + ? node.tag === HostResource + : false) || + (enableHostSingletons && supportsSingletons + ? node.tag === HostSingleton + : false) + ) { + if (hostSubtreeRoot === null) { + hostSubtreeRoot = node; + 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) { + try { + const instance = node.stateNode; + if (isHidden) { + hideTextInstance(instance); + } else { + unhideTextInstance(instance, node.memoizedProps); + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } else if ( + (node.tag === OffscreenComponent || + node.tag === LegacyHiddenComponent) && + (node.memoizedState: OffscreenState) !== null && + node !== finishedWork + ) { + // Found a nested Offscreen component that is hidden. + // Don't search any deeper. This tree should remain hidden. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + + if (node === finishedWork) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === finishedWork) { + return; + } + + if (hostSubtreeRoot === node) { + hostSubtreeRoot = null; + } + + node = node.return; + } + + if (hostSubtreeRoot === node) { + hostSubtreeRoot = null; + } + + node.sibling.return = node.return; + node = node.sibling; + } + } +} + +function commitAttachRef(finishedWork: Fiber) { + const ref = finishedWork.ref; + if (ref !== null) { + const instance = finishedWork.stateNode; + let instanceToUse; + switch (finishedWork.tag) { + case HostResource: + case HostSingleton: + case HostComponent: + instanceToUse = getPublicInstance(instance); + break; + default: + instanceToUse = instance; + } + // Moved outside to ensure DCE works with this flag + if (enableScopeAPI && finishedWork.tag === ScopeComponent) { + instanceToUse = instance; + } + if (typeof ref === 'function') { + if (shouldProfile(finishedWork)) { + try { + startLayoutEffectTimer(); + finishedWork.refCleanup = ref(instanceToUse); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + finishedWork.refCleanup = ref(instanceToUse); + } + } else { + if (__DEV__) { + if (!ref.hasOwnProperty('current')) { + console.error( + 'Unexpected ref object provided for %s. ' + + 'Use either a ref-setter function or React.createRef().', + getComponentNameFromFiber(finishedWork), + ); + } + } + + // $FlowFixMe unable to narrow type to the non-function case + ref.current = instanceToUse; + } + } +} + +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. + // It also prevents events from bubbling from within disconnected components. + // + // Ideally, we should also clear the child pointer of the parent alternate to let this + // get GC:ed but we don't know which for sure which parent is the current + // one so we'll settle for GC:ing the subtree of this child. + // This child itself will be GC:ed when the parent updates the next time. + // + // Note that we can't clear child or sibling pointers yet. + // They're needed for passive effects and for findDOMNode. + // We defer those fields, and all other cleanup, to the passive phase (see detachFiberAfterEffects). + // + // Don't reset the alternate yet, either. We need that so we can detach the + // alternate's fields in the passive phase. Clearing the return pointer is + // sufficient for findDOMNode semantics. + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.return = null; + } + fiber.return = null; +} + +function detachFiberAfterEffects(fiber: Fiber) { + const alternate = fiber.alternate; + if (alternate !== null) { + fiber.alternate = null; + detachFiberAfterEffects(alternate); + } + + // Note: Defensively using negation instead of < in case + // `deletedTreeCleanUpLevel` is undefined. + if (!(deletedTreeCleanUpLevel >= 2)) { + // This is the default branch (level 0). + fiber.child = null; + fiber.deletions = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.sibling = null; + fiber.stateNode = null; + fiber.updateQueue = null; + + if (__DEV__) { + fiber._debugOwner = null; + } + } else { + // Clear cyclical Fiber fields. This level alone is designed to roughly + // approximate the planned Fiber refactor. In that world, `setState` will be + // bound to a special "instance" object instead of a Fiber. The Instance + // object will not have any of these fields. It will only be connected to + // the fiber tree via a single link at the root. So if this level alone is + // sufficient to fix memory issues, that bodes well for our plans. + fiber.child = null; + fiber.deletions = null; + fiber.sibling = null; + + // The `stateNode` is cyclical because on host nodes it points to the host + // tree, which has its own pointers to children, parents, and siblings. + // The other host nodes also point back to fibers, so we should detach that + // one, too. + if (fiber.tag === HostComponent) { + const hostInstance: Instance = fiber.stateNode; + if (hostInstance !== null) { + detachDeletedInstance(hostInstance); + } + } + fiber.stateNode = null; + + // I'm intentionally not clearing the `return` field in this level. We + // already disconnect the `return` pointer at the root of the deleted + // subtree (in `detachFiberMutation`). Besides, `return` by itself is not + // cyclical — it's only cyclical when combined with `child`, `sibling`, and + // `alternate`. But we'll clear it in the next level anyway, just in case. + + if (__DEV__) { + fiber._debugOwner = null; + } + + if (deletedTreeCleanUpLevel >= 3) { + // Theoretically, nothing in here should be necessary, because we already + // disconnected the fiber from the tree. So even if something leaks this + // particular fiber, it won't leak anything else + // + // The purpose of this branch is to be super aggressive so we can measure + // if there's any difference in memory impact. If there is, that could + // indicate a React leak we don't know about. + fiber.return = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.stateNode = null; + // TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead. + fiber.updateQueue = null; + } + } +} + +function emptyPortalContainer(current: Fiber) { + if (!supportsPersistence) { + return; + } + + const portal: { + containerInfo: Container, + pendingChildren: ChildSet, + ... + } = current.stateNode; + const {containerInfo} = portal; + const emptyChildSet = createContainerChildSet(containerInfo); + replaceContainerChildren(containerInfo, emptyChildSet); +} + +function getHostParentFiber(fiber: Fiber): Fiber { + let parent = fiber.return; + while (parent !== null) { + if (isHostParent(parent)) { + return parent; + } + parent = parent.return; + } + + throw new Error( + 'Expected to find a host parent. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); +} + +function isHostParent(fiber: Fiber): boolean { + return ( + fiber.tag === HostComponent || + fiber.tag === HostRoot || + (enableFloat && supportsResources ? fiber.tag === HostResource : false) || + (enableHostSingletons && supportsSingletons + ? fiber.tag === HostSingleton + : false) || + fiber.tag === HostPortal + ); +} + +function getHostSibling(fiber: Fiber): ?Instance { + // We're going to search forward into the tree until we find a sibling host + // node. Unfortunately, if multiple insertions are done in a row we have to + // search past them. This leads to exponential search for the next sibling. + // TODO: Find a more efficient way to do this. + let node: Fiber = fiber; + siblings: while (true) { + // If we didn't find anything, let's try the next sibling. + while (node.sibling === null) { + if (node.return === null || isHostParent(node.return)) { + // If we pop out of the root or hit the parent the fiber we are the + // last sibling. + return null; + } + // $FlowFixMe[incompatible-type] found when upgrading Flow + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + while ( + node.tag !== HostComponent && + node.tag !== HostText && + (!(enableHostSingletons && supportsSingletons) + ? true + : node.tag !== HostSingleton) && + node.tag !== DehydratedFragment + ) { + // If it is not host node and, we might have a host node inside it. + // Try to search down until we find one. + if (node.flags & Placement) { + // If we don't have a child, try the siblings instead. + continue siblings; + } + // If we don't have a child, try the siblings instead. + // We also skip portals because they are not part of this host tree. + if (node.child === null || node.tag === HostPortal) { + continue siblings; + } else { + node.child.return = node; + node = node.child; + } + } + // Check if this host node is stable or about to be placed. + if (!(node.flags & Placement)) { + // Found it! + return node.stateNode; + } + } +} + +function commitPlacement(finishedWork: Fiber): void { + if (!supportsMutation) { + return; + } + + if (enableHostSingletons && supportsSingletons) { + if (finishedWork.tag === HostSingleton) { + // Singletons are already in the Host and don't need to be placed + // Since they operate somewhat like Portals though their children will + // have Placement and will get placed inside them + return; + } + } + // Recursively insert all host nodes into the parent. + const parentFiber = getHostParentFiber(finishedWork); + + switch (parentFiber.tag) { + case HostSingleton: { + if (enableHostSingletons && supportsSingletons) { + const parent: Instance = parentFiber.stateNode; + const before = getHostSibling(finishedWork); + // We only have the top Fiber that was inserted but we need to recurse down its + // children to find all the terminal nodes. + insertOrAppendPlacementNode(finishedWork, before, parent); + break; + } + } + // eslint-disable-next-line no-fallthrough + case HostComponent: { + const parent: Instance = parentFiber.stateNode; + if (parentFiber.flags & ContentReset) { + // Reset the text content of the parent before doing any insertions + resetTextContent(parent); + // Clear ContentReset from the effect tag + parentFiber.flags &= ~ContentReset; + } + + const before = getHostSibling(finishedWork); + // We only have the top Fiber that was inserted but we need to recurse down its + // children to find all the terminal nodes. + insertOrAppendPlacementNode(finishedWork, before, parent); + break; + } + case HostRoot: + case HostPortal: { + const parent: Container = parentFiber.stateNode.containerInfo; + const before = getHostSibling(finishedWork); + insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); + break; + } + // eslint-disable-next-line-no-fallthrough + default: + throw new Error( + 'Invalid host parent fiber. This error is likely caused by a bug ' + + 'in React. Please file an issue.', + ); + } +} + +function insertOrAppendPlacementNodeIntoContainer( + node: Fiber, + before: ?Instance, + parent: Container, +): void { + const {tag} = node; + const isHost = tag === HostComponent || tag === HostText; + if (isHost) { + const stateNode = node.stateNode; + if (before) { + insertInContainerBefore(parent, stateNode, before); + } else { + appendChildToContainer(parent, stateNode); + } + } else if ( + tag === HostPortal || + (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false) + ) { + // If the insertion itself is a portal, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + // If the insertion is a HostSingleton then it will be placed independently + } else { + const child = node.child; + if (child !== null) { + insertOrAppendPlacementNodeIntoContainer(child, before, parent); + let sibling = child.sibling; + while (sibling !== null) { + insertOrAppendPlacementNodeIntoContainer(sibling, before, parent); + sibling = sibling.sibling; + } + } + } +} + +function insertOrAppendPlacementNode( + node: Fiber, + before: ?Instance, + parent: Instance, +): void { + const {tag} = node; + const isHost = tag === HostComponent || tag === HostText; + if (isHost) { + const stateNode = node.stateNode; + if (before) { + insertBefore(parent, stateNode, before); + } else { + appendChild(parent, stateNode); + } + } else if ( + tag === HostPortal || + (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false) + ) { + // If the insertion itself is a portal, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + // If the insertion is a HostSingleton then it will be placed independently + } else { + const child = node.child; + if (child !== null) { + insertOrAppendPlacementNode(child, before, parent); + let sibling = child.sibling; + while (sibling !== null) { + insertOrAppendPlacementNode(sibling, before, parent); + sibling = sibling.sibling; + } + } + } +} + +// 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; + +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. + + // 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: null | Fiber = returnFiber; + findParent: while (parent !== null) { + switch (parent.tag) { + case HostSingleton: + 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); + + // 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 HostResource: { + if (enableFloat && supportsResources) { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + if (deletedFiber.memoizedState) { + releaseResource(deletedFiber.memoizedState); + } + return; + } + } + // eslint-disable-next-line no-fallthrough + case HostSingleton: { + if (enableHostSingletons && supportsSingletons) { + if (!offscreenSubtreeWasHidden) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + + const prevHostParent = hostParent; + const prevHostParentIsContainer = hostParentIsContainer; + hostParent = deletedFiber.stateNode; + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + + // Normally this is called in passive unmount effect phase however with + // HostSingleton we warn if you acquire one that is already associated to + // a different fiber. To increase our chances of avoiding this, specifically + // if you keyed a HostSingleton so there will be a delete followed by a Placement + // we treat detach eagerly here + releaseSingletonInstance(deletedFiber.stateNode); + + hostParent = prevHostParent; + hostParentIsContainer = prevHostParentIsContainer; + + return; + } + } + // eslint-disable-next-line no-fallthrough + 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 { + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + } + return; + } + case DehydratedFragment: { + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onDeleted = hydrationCallbacks.onDeleted; + if (onDeleted) { + onDeleted((deletedFiber.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 { + emptyPortalContainer(deletedFiber); + + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + } + 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 (shouldProfile(deletedFiber)) { + startLayoutEffectTimer(); + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + recordLayoutEffectDuration(deletedFiber); + } else { + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } + + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } + } + + 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; + } + case ScopeComponent: { + if (enableScopeAPI) { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + } + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + return; + } + case OffscreenComponent: { + safelyDetachRef(deletedFiber, nearestMountedAncestor); + if (deletedFiber.mode & ConcurrentMode) { + // 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; + } else { + recursivelyTraverseDeletionEffects( + finishedRoot, + nearestMountedAncestor, + deletedFiber, + ); + } + 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)); + } + } else if (__DEV__) { + if (suspenseCallback !== undefined) { + console.error('Unexpected type for suspenseCallback.'); + } + } + } +} + +function commitSuspenseHydrationCallbacks( + finishedRoot: FiberRoot, + finishedWork: Fiber, +) { + if (!supportsHydration) { + return; + } + const newState: SuspenseState | null = finishedWork.memoizedState; + if (newState === null) { + const current = finishedWork.alternate; + if (current !== null) { + const prevState: SuspenseState | null = current.memoizedState; + if (prevState !== null) { + const suspenseInstance = prevState.dehydrated; + if (suspenseInstance !== null) { + try { + commitHydratedSuspenseInstance(suspenseInstance); + if (enableSuspenseCallback) { + const hydrationCallbacks = finishedRoot.hydrationCallbacks; + if (hydrationCallbacks !== null) { + const onHydrated = hydrationCallbacks.onHydrated; + if (onHydrated) { + onHydrated(suspenseInstance); + } + } + } + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + } + } +} + +function getRetryCache(finishedWork) { + // TODO: Unify the interface for the retry cache so we don't have to switch + // on the tag like this. + switch (finishedWork.tag) { + case SuspenseComponent: + case SuspenseListComponent: { + let retryCache = finishedWork.stateNode; + if (retryCache === null) { + retryCache = finishedWork.stateNode = new PossiblyWeakSet(); + } + return retryCache; + } + case OffscreenComponent: { + const instance: OffscreenInstance = finishedWork.stateNode; + // $FlowFixMe[incompatible-type-arg] found when upgrading Flow + let retryCache: null | Set | WeakSet = + // $FlowFixMe[incompatible-type] found when upgrading Flow + instance._retryCache; + if (retryCache === null) { + // $FlowFixMe[incompatible-type] + retryCache = instance._retryCache = new PossiblyWeakSet(); + } + return retryCache; + } + default: { + throw new Error( + `Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` + + 'bug in React.', + ); + } + } +} + +export function detachOffscreenInstance(instance: OffscreenInstance): void { + const currentOffscreenFiber = instance._current; + if (currentOffscreenFiber === null) { + throw new Error( + 'Calling Offscreen.detach before instance handle has been set.', + ); + } + + const _detachOffscreen = () => { + instance._visibility |= OffscreenDetached; + let node = currentOffscreenFiber.child?.sibling; + while (node != null) { + disappearLayoutEffects(node); + disconnectPassiveEffect(node); + node = node.sibling; + } + }; + + const executionContext = getExecutionContext(); + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { + scheduleMicrotask(_detachOffscreen); + } else { + _detachOffscreen(); + } +} + +export function attachOffscreenInstance(instance: OffscreenInstance): void { + const fiber = instance._current; + if (fiber === null) { + throw new Error( + 'Calling Offscreen.detach before instance handle has been set.', + ); + } + + instance._visibility &= ~OffscreenDetached; + + const executionContext = getExecutionContext(); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } else { + reappearLayoutEffects(root, fiber.alternate, fiber, false); + reconnectPassiveEffects(root, fiber, NoLanes, null, false); + } + } +} + +function attachSuspenseRetryListeners( + finishedWork: Fiber, + wakeables: Set, +) { + // If this boundary just timed out, then it will have a set of wakeables. + // For each wakeable, attach a listener so that when it resolves, React + // attempts to re-render the boundary in the primary (pre-timeout) state. + const retryCache = getRetryCache(finishedWork); + wakeables.forEach(wakeable => { + // Memoize using the boundary fiber to prevent redundant listeners. + const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); + if (!retryCache.has(wakeable)) { + retryCache.add(wakeable); + + if (enableUpdaterTracking) { + if (isDevToolsPresent) { + if (inProgressLanes !== null && inProgressRoot !== null) { + // If we have pending work still, associate the original updaters with it. + restorePendingUpdaters(inProgressRoot, inProgressLanes); + } else { + throw Error( + 'Expected finished root and lanes to be set. This is a bug in React.', + ); + } + } + } + + wakeable.then(retry, retry); + } + }); +} + +// This function detects when a Suspense boundary goes from visible to hidden. +// It returns false if the boundary is already hidden. +// TODO: Use an effect tag. +export function isSuspenseBoundaryBeingHidden( + current: Fiber | null, + finishedWork: Fiber, +): boolean { + if (current !== null) { + const oldState: SuspenseState | null = current.memoizedState; + if (oldState === null || oldState.dehydrated !== null) { + const newState: SuspenseState | null = finishedWork.memoizedState; + return newState !== null && newState.dehydrated === null; + } + } + return false; +} + +export function commitMutationEffects( + root: FiberRoot, + finishedWork: Fiber, + committedLanes: Lanes, +) { + inProgressLanes = committedLanes; + inProgressRoot = root; + + setCurrentDebugFiberInDEV(finishedWork); + commitMutationEffectsOnFiber(finishedWork, root, committedLanes); + setCurrentDebugFiberInDEV(finishedWork); + + inProgressLanes = null; + inProgressRoot = null; +} + +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 prevDebugFiber = getCurrentDebugFiberInDEV(); + if (parentFiber.subtreeFlags & MutationMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + commitMutationEffectsOnFiber(child, root, lanes); + child = child.sibling; + } + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +function commitMutationEffectsOnFiber( + finishedWork: Fiber, + root: FiberRoot, + lanes: Lanes, +) { + const current = finishedWork.alternate; + const flags = finishedWork.flags; + + // 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 reconciliation, 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 & 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 (shouldProfile(finishedWork)) { + 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; + } + case ClassComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + + if (flags & Callback && offscreenSubtreeIsHidden) { + const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + deferHiddenCallbacks(updateQueue); + } + } + return; + } + case HostResource: { + if (enableFloat && supportsResources) { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + + if (flags & Update) { + const newResource = finishedWork.memoizedState; + if (current !== null) { + const currentResource = current.memoizedState; + if (currentResource !== newResource) { + releaseResource(currentResource); + } + } + finishedWork.stateNode = newResource + ? acquireResource(newResource) + : null; + } + return; + } + } + // eslint-disable-next-line-no-fallthrough + case HostSingleton: { + if (enableHostSingletons && supportsSingletons) { + if (flags & Update) { + const previousWork = finishedWork.alternate; + if (previousWork === null) { + const singleton = finishedWork.stateNode; + const props = finishedWork.memoizedProps; + // This was a new mount, we need to clear and set initial properties + clearSingleton(singleton); + acquireSingletonInstance( + finishedWork.type, + props, + singleton, + finishedWork, + ); + } + } + } + } + // eslint-disable-next-line-no-fallthrough + 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 & 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); + + const offscreenFiber: Fiber = (finishedWork.child: any); + + if (offscreenFiber.flags & Visibility) { + const newState: OffscreenState | null = offscreenFiber.memoizedState; + const isHidden = newState !== null; + if (isHidden) { + const wasHidden = + offscreenFiber.alternate !== null && + offscreenFiber.alternate.memoizedState !== null; + if (!wasHidden) { + // TODO: Move to passive phase + markCommitTimeOfFallback(); + } + } + } + + if (flags & Update) { + try { + commitSuspenseCallback(finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + const wakeables: Set | null = (finishedWork.updateQueue: any); + if (wakeables !== null) { + finishedWork.updateQueue = null; + attachSuspenseRetryListeners(finishedWork, wakeables); + } + } + return; + } + case OffscreenComponent: { + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + + const newState: OffscreenState | null = finishedWork.memoizedState; + const isHidden = newState !== null; + const wasHidden = current !== null && current.memoizedState !== null; + + if (finishedWork.mode & ConcurrentMode) { + // 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 prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden; + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; + offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden || isHidden; + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || wasHidden; + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; + offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden; + } else { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + } + + commitReconciliationEffects(finishedWork); + // TODO: Add explicit effect flag to set _current. + finishedWork.stateNode._current = finishedWork; + + if (flags & Visibility) { + const offscreenInstance: OffscreenInstance = finishedWork.stateNode; + + // Track the current state on the Offscreen instance so we can + // read it during an event + if (isHidden) { + offscreenInstance._visibility &= ~OffscreenVisible; + } else { + offscreenInstance._visibility |= OffscreenVisible; + } + + if (isHidden) { + const isUpdate = current !== null; + const wasHiddenByAncestorOffscreen = + offscreenSubtreeIsHidden || offscreenSubtreeWasHidden; + // Only trigger disapper layout effects if: + // - This is an update, not first mount. + // - This Offscreen was not hidden before. + // - Ancestor Offscreen was not hidden in previous commit. + if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) { + if ((finishedWork.mode & ConcurrentMode) !== NoMode) { + // Disappear the layout effects of all the children + recursivelyTraverseDisappearLayoutEffects(finishedWork); + } + } + } else { + if (wasHidden) { + // TODO: Move re-appear call here for symmetry? + } + } + + // Offscreen with manual mode manages visibility manually. + if (supportsMutation && !isOffscreenManual(finishedWork)) { + // TODO: This needs to run whenever there's an insertion or update + // inside a hidden Offscreen tree. + hideOrUnhideAllChildren(finishedWork, isHidden); + } + } + + // TODO: Move to passive phase + if (flags & Update) { + const offscreenQueue: OffscreenQueue | null = (finishedWork.updateQueue: any); + if (offscreenQueue !== null) { + const wakeables = offscreenQueue.wakeables; + if (wakeables !== null) { + offscreenQueue.wakeables = null; + attachSuspenseRetryListeners(finishedWork, wakeables); + } + } + } + return; + } + case SuspenseListComponent: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + if (flags & Update) { + const wakeables: Set | null = (finishedWork.updateQueue: any); + if (wakeables !== null) { + finishedWork.updateQueue = null; + attachSuspenseRetryListeners(finishedWork, wakeables); + } + } + return; + } + case ScopeComponent: { + if (enableScopeAPI) { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + // 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; + } + default: { + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork); + + return; + } + } +} +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; + } +} + +export function commitLayoutEffects( + finishedWork: Fiber, + root: FiberRoot, + committedLanes: Lanes, +): void { + inProgressLanes = committedLanes; + inProgressRoot = root; + + const current = finishedWork.alternate; + commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes); + + inProgressLanes = null; + inProgressRoot = null; +} + +function recursivelyTraverseLayoutEffects( + root: FiberRoot, + parentFiber: Fiber, + lanes: Lanes, +) { + const prevDebugFiber = getCurrentDebugFiberInDEV(); + if (parentFiber.subtreeFlags & LayoutMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + const current = child.alternate; + commitLayoutEffectOnFiber(root, current, child, lanes); + child = child.sibling; + } + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +export function disappearLayoutEffects(finishedWork: Fiber) { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + // TODO (Offscreen) Check: flags & LayoutStatic + if (shouldProfile(finishedWork)) { + try { + startLayoutEffectTimer(); + commitHookEffectListUnmount( + HookLayout, + finishedWork, + finishedWork.return, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + commitHookEffectListUnmount( + HookLayout, + finishedWork, + finishedWork.return, + ); + } + + recursivelyTraverseDisappearLayoutEffects(finishedWork); + break; + } + case ClassComponent: { + // TODO (Offscreen) Check: flags & RefStatic + safelyDetachRef(finishedWork, finishedWork.return); + + const instance = finishedWork.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount( + finishedWork, + finishedWork.return, + instance, + ); + } + + recursivelyTraverseDisappearLayoutEffects(finishedWork); + break; + } + case HostResource: + case HostSingleton: + case HostComponent: { + // TODO (Offscreen) Check: flags & RefStatic + safelyDetachRef(finishedWork, finishedWork.return); + + recursivelyTraverseDisappearLayoutEffects(finishedWork); + break; + } + case OffscreenComponent: { + // TODO (Offscreen) Check: flags & RefStatic + safelyDetachRef(finishedWork, finishedWork.return); + + const isHidden = finishedWork.memoizedState !== null; + if (isHidden) { + // Nested Offscreen tree is already hidden. Don't disappear + // its effects. + } else { + recursivelyTraverseDisappearLayoutEffects(finishedWork); + } + break; + } + default: { + recursivelyTraverseDisappearLayoutEffects(finishedWork); + break; + } + } +} + +function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) { + // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic) + let child = parentFiber.child; + while (child !== null) { + disappearLayoutEffects(child); + child = child.sibling; + } +} + +export function reappearLayoutEffects( + finishedRoot: FiberRoot, + current: Fiber | null, + finishedWork: Fiber, + // This function visits both newly finished work and nodes that were re-used + // from a previously committed tree. We cannot check non-static flags if the + // node was reused. + includeWorkInProgressEffects: boolean, +) { + // Turn on layout effects in a tree that previously disappeared. + const flags = finishedWork.flags; + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + // TODO: Check flags & LayoutStatic + commitHookLayoutEffects(finishedWork, HookLayout); + break; + } + case ClassComponent: { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + + // TODO: Check for LayoutStatic flag + const instance = finishedWork.stateNode; + if (typeof instance.componentDidMount === 'function') { + try { + instance.componentDidMount(); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + + // Commit any callbacks that would have fired while the component + // was hidden. + const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + commitHiddenCallbacks(updateQueue, instance); + } + + // If this is newly finished work, check for setState callbacks + if (includeWorkInProgressEffects && flags & Callback) { + commitClassCallbacks(finishedWork); + } + + // TODO: Check flags & RefStatic + safelyAttachRef(finishedWork, finishedWork.return); + break; + } + // Unlike commitLayoutEffectsOnFiber, we don't need to handle HostRoot + // because this function only visits nodes that are inside an + // Offscreen fiber. + // case HostRoot: { + // ... + // } + case HostResource: + case HostSingleton: + case HostComponent: { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + + // Renderers may schedule work to be done after host components are mounted + // (eg DOM renderer may schedule auto-focus for inputs and form controls). + // These effects should only be committed when components are first mounted, + // aka when there is no current/alternate. + if (includeWorkInProgressEffects && current === null && flags & Update) { + commitHostComponentMount(finishedWork); + } + + // TODO: Check flags & Ref + safelyAttachRef(finishedWork, finishedWork.return); + break; + } + case Profiler: { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + // TODO: Figure out how Profiler updates should work with Offscreen + if (includeWorkInProgressEffects && flags & Update) { + commitProfilerUpdate(finishedWork, current); + } + break; + } + case SuspenseComponent: { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + + // TODO: Figure out how Suspense hydration callbacks should work + // with Offscreen. + if (includeWorkInProgressEffects && flags & Update) { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + } + break; + } + case OffscreenComponent: { + const offscreenState: OffscreenState = finishedWork.memoizedState; + const isHidden = offscreenState !== null; + if (isHidden) { + // Nested Offscreen tree is still hidden. Don't re-appear its effects. + } else { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + } + // TODO: Check flags & Ref + safelyAttachRef(finishedWork, finishedWork.return); + break; + } + default: { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + break; + } + } +} + +function recursivelyTraverseReappearLayoutEffects( + finishedRoot: FiberRoot, + parentFiber: Fiber, + includeWorkInProgressEffects: boolean, +) { + // This function visits both newly finished work and nodes that were re-used + // from a previously committed tree. We cannot check non-static flags if the + // node was reused. + const childShouldIncludeWorkInProgressEffects = + includeWorkInProgressEffects && + (parentFiber.subtreeFlags & LayoutMask) !== NoFlags; + + // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic) + const prevDebugFiber = getCurrentDebugFiberInDEV(); + let child = parentFiber.child; + while (child !== null) { + const current = child.alternate; + reappearLayoutEffects( + finishedRoot, + current, + child, + childShouldIncludeWorkInProgressEffects, + ); + child = child.sibling; + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +function commitHookPassiveMountEffects( + finishedWork: Fiber, + hookFlags: HookFlags, +) { + if (shouldProfile(finishedWork)) { + startPassiveEffectTimer(); + try { + commitHookEffectListMount(hookFlags, finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + recordPassiveEffectDuration(finishedWork); + } else { + try { + commitHookEffectListMount(hookFlags, finishedWork); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } +} + +function commitOffscreenPassiveMountEffects( + current: Fiber | null, + finishedWork: Fiber, + instance: OffscreenInstance, +) { + if (enableCache) { + let previousCache: Cache | null = null; + if ( + current !== null && + current.memoizedState !== null && + current.memoizedState.cachePool !== null + ) { + previousCache = current.memoizedState.cachePool.pool; + } + let nextCache: Cache | null = null; + if ( + finishedWork.memoizedState !== null && + finishedWork.memoizedState.cachePool !== null + ) { + nextCache = finishedWork.memoizedState.cachePool.pool; + } + // Retain/release the cache used for pending (suspended) nodes. + // Note that this is only reached in the non-suspended/visible case: + // when the content is suspended/hidden, the retain/release occurs + // via the parent Suspense component (see case above). + if (nextCache !== previousCache) { + if (nextCache != null) { + retainCache(nextCache); + } + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + + if (enableTransitionTracing) { + // TODO: Pre-rendering should not be counted as part of a transition. We + // may add separate logs for pre-rendering, but it's not part of the + // primary metrics. + const offscreenState: OffscreenState = finishedWork.memoizedState; + const queue: OffscreenQueue | null = (finishedWork.updateQueue: any); + + const isHidden = offscreenState !== null; + if (queue !== null) { + if (isHidden) { + const transitions = queue.transitions; + if (transitions !== null) { + transitions.forEach(transition => { + // Add all the transitions saved in the update queue during + // the render phase (ie the transitions associated with this boundary) + // into the transitions set. + if (instance._transitions === null) { + instance._transitions = new Set(); + } + instance._transitions.add(transition); + }); + } + + const markerInstances = queue.markerInstances; + if (markerInstances !== null) { + markerInstances.forEach(markerInstance => { + const markerTransitions = markerInstance.transitions; + // There should only be a few tracing marker transitions because + // they should be only associated with the transition that + // caused them + if (markerTransitions !== null) { + markerTransitions.forEach(transition => { + if (instance._transitions === null) { + instance._transitions = new Set(); + } else if (instance._transitions.has(transition)) { + if (markerInstance.pendingBoundaries === null) { + markerInstance.pendingBoundaries = new Map(); + } + if (instance._pendingMarkers === null) { + instance._pendingMarkers = new Set(); + } + + instance._pendingMarkers.add(markerInstance); + } + }); + } + }); + } + } + + finishedWork.updateQueue = null; + } + + commitTransitionProgress(finishedWork); + + // TODO: Refactor this into an if/else branch + if (!isHidden) { + instance._transitions = null; + instance._pendingMarkers = null; + } + } +} + +function commitCachePassiveMountEffect( + current: Fiber | null, + finishedWork: Fiber, +) { + if (enableCache) { + let previousCache: Cache | null = null; + if (finishedWork.alternate !== null) { + previousCache = finishedWork.alternate.memoizedState.cache; + } + const nextCache = finishedWork.memoizedState.cache; + // Retain/release the cache. In theory the cache component + // could be "borrowing" a cache instance owned by some parent, + // in which case we could avoid retaining/releasing. But it + // is non-trivial to determine when that is the case, so we + // always retain/release. + if (nextCache !== previousCache) { + retainCache(nextCache); + if (previousCache != null) { + releaseCache(previousCache); + } + } + } +} + +function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) { + // Get the transitions that were initiatized during the render + // and add a start transition callback for each of them + // We will only call this on initial mount of the tracing marker + // only if there are no suspense children + const instance = finishedWork.stateNode; + if (instance.transitions !== null && instance.pendingBoundaries === null) { + addMarkerCompleteCallbackToPendingTransition( + finishedWork.memoizedProps.name, + instance.transitions, + ); + instance.transitions = null; + instance.pendingBoundaries = null; + instance.aborts = null; + instance.name = null; + } +} + +export function commitPassiveMountEffects( + root: FiberRoot, + finishedWork: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, +): void { + setCurrentDebugFiberInDEV(finishedWork); + commitPassiveMountOnFiber( + root, + finishedWork, + committedLanes, + committedTransitions, + ); + resetCurrentDebugFiberInDEV(); +} + +function recursivelyTraversePassiveMountEffects( + root: FiberRoot, + parentFiber: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, +) { + const prevDebugFiber = getCurrentDebugFiberInDEV(); + if (parentFiber.subtreeFlags & PassiveMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + commitPassiveMountOnFiber( + root, + child, + committedLanes, + committedTransitions, + ); + child = child.sibling; + } + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +function commitPassiveMountOnFiber( + finishedRoot: FiberRoot, + finishedWork: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, +): void { + // When updating this function, also update reconnectPassiveEffects, which does + // most of the same things when an offscreen tree goes from hidden -> visible, + // or when toggling effects inside a hidden tree. + const flags = finishedWork.flags; + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + if (flags & Passive) { + commitHookPassiveMountEffects( + finishedWork, + HookPassive | HookHasEffect, + ); + } + break; + } + case HostRoot: { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + if (flags & Passive) { + if (enableCache) { + let previousCache: Cache | null = null; + if (finishedWork.alternate !== null) { + previousCache = finishedWork.alternate.memoizedState.cache; + } + const nextCache = finishedWork.memoizedState.cache; + // Retain/release the root cache. + // Note that on initial mount, previousCache and nextCache will be the same + // and this retain won't occur. To counter this, we instead retain the HostRoot's + // initial cache when creating the root itself (see createFiberRoot() in + // ReactFiberRoot.js). Subsequent updates that change the cache are reflected + // here, such that previous/next caches are retained correctly. + if (nextCache !== previousCache) { + retainCache(nextCache); + if (previousCache != null) { + releaseCache(previousCache); + } + } + } + + if (enableTransitionTracing) { + // Get the transitions that were initiatized during the render + // and add a start transition callback for each of them + const root: FiberRoot = finishedWork.stateNode; + const incompleteTransitions = root.incompleteTransitions; + // Initial render + if (committedTransitions !== null) { + committedTransitions.forEach(transition => { + addTransitionStartCallbackToPendingTransition(transition); + }); + + clearTransitionsForLanes(finishedRoot, committedLanes); + } + + incompleteTransitions.forEach((markerInstance, transition) => { + const pendingBoundaries = markerInstance.pendingBoundaries; + if (pendingBoundaries === null || pendingBoundaries.size === 0) { + if (markerInstance.aborts === null) { + addTransitionCompleteCallbackToPendingTransition(transition); + } + incompleteTransitions.delete(transition); + } + }); + + clearTransitionsForLanes(finishedRoot, committedLanes); + } + } + break; + } + case LegacyHiddenComponent: { + if (enableLegacyHidden) { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + + if (flags & Passive) { + const current = finishedWork.alternate; + const instance: OffscreenInstance = finishedWork.stateNode; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); + } + } + break; + } + case OffscreenComponent: { + // TODO: Pass `current` as argument to this function + const instance: OffscreenInstance = finishedWork.stateNode; + const nextState: OffscreenState | null = finishedWork.memoizedState; + + const isHidden = nextState !== null; + + if (isHidden) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { + // The effects are currently connected. Update them. + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + } else { + if (finishedWork.mode & ConcurrentMode) { + // The effects are currently disconnected. Since the tree is hidden, + // don't connect them. This also applies to the initial render. + if (enableCache || enableTransitionTracing) { + // "Atomic" effects are ones that need to fire on every commit, + // even during pre-rendering. An example is updating the reference + // count on cache instances. + recursivelyTraverseAtomicPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + } + } else { + // Legacy Mode: Fire the effects even if the tree is hidden. + instance._visibility |= OffscreenPassiveEffectsConnected; + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + } + } + } else { + // Tree is visible + if (instance._visibility & OffscreenPassiveEffectsConnected) { + // The effects are currently connected. Update them. + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + } else { + // The effects are currently disconnected. Reconnect them, while also + // firing effects inside newly mounted trees. This also applies to + // the initial render. + instance._visibility |= OffscreenPassiveEffectsConnected; + + const includeWorkInProgressEffects = + (finishedWork.subtreeFlags & PassiveMask) !== NoFlags; + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + } + } + + if (flags & Passive) { + const current = finishedWork.alternate; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); + } + break; + } + case CacheComponent: { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + if (flags & Passive) { + // TODO: Pass `current` as argument to this function + const current = finishedWork.alternate; + commitCachePassiveMountEffect(current, finishedWork); + } + break; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + if (flags & Passive) { + commitTracingMarkerPassiveMountEffect(finishedWork); + } + break; + } + // Intentional fallthrough to next branch + } + // eslint-disable-next-line-no-fallthrough + default: { + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + break; + } + } +} + +function recursivelyTraverseReconnectPassiveEffects( + finishedRoot: FiberRoot, + parentFiber: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, + includeWorkInProgressEffects: boolean, +) { + // This function visits both newly finished work and nodes that were re-used + // from a previously committed tree. We cannot check non-static flags if the + // node was reused. + const childShouldIncludeWorkInProgressEffects = + includeWorkInProgressEffects && + (parentFiber.subtreeFlags & PassiveMask) !== NoFlags; + + // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic) + const prevDebugFiber = getCurrentDebugFiberInDEV(); + let child = parentFiber.child; + while (child !== null) { + reconnectPassiveEffects( + finishedRoot, + child, + committedLanes, + committedTransitions, + childShouldIncludeWorkInProgressEffects, + ); + child = child.sibling; + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +export function reconnectPassiveEffects( + finishedRoot: FiberRoot, + finishedWork: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, + // This function visits both newly finished work and nodes that were re-used + // from a previously committed tree. We cannot check non-static flags if the + // node was reused. + includeWorkInProgressEffects: boolean, +) { + const flags = finishedWork.flags; + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + // TODO: Check for PassiveStatic flag + commitHookPassiveMountEffects(finishedWork, HookPassive); + break; + } + // Unlike commitPassiveMountOnFiber, we don't need to handle HostRoot + // because this function only visits nodes that are inside an + // Offscreen fiber. + // case HostRoot: { + // ... + // } + case LegacyHiddenComponent: { + if (enableLegacyHidden) { + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + + if (includeWorkInProgressEffects && flags & Passive) { + // TODO: Pass `current` as argument to this function + const current: Fiber | null = finishedWork.alternate; + const instance: OffscreenInstance = finishedWork.stateNode; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); + } + } + break; + } + case OffscreenComponent: { + const instance: OffscreenInstance = finishedWork.stateNode; + const nextState: OffscreenState | null = finishedWork.memoizedState; + + const isHidden = nextState !== null; + + if (isHidden) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { + // The effects are currently connected. Update them. + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + } else { + if (finishedWork.mode & ConcurrentMode) { + // The effects are currently disconnected. Since the tree is hidden, + // don't connect them. This also applies to the initial render. + if (enableCache || enableTransitionTracing) { + // "Atomic" effects are ones that need to fire on every commit, + // even during pre-rendering. An example is updating the reference + // count on cache instances. + recursivelyTraverseAtomicPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + } + } else { + // Legacy Mode: Fire the effects even if the tree is hidden. + instance._visibility |= OffscreenPassiveEffectsConnected; + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + } + } + } else { + // Tree is visible + + // Since we're already inside a reconnecting tree, it doesn't matter + // whether the effects are currently connected. In either case, we'll + // continue traversing the tree and firing all the effects. + // + // We do need to set the "connected" flag on the instance, though. + instance._visibility |= OffscreenPassiveEffectsConnected; + + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + } + + if (includeWorkInProgressEffects && flags & Passive) { + // TODO: Pass `current` as argument to this function + const current: Fiber | null = finishedWork.alternate; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); + } + break; + } + case CacheComponent: { + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + if (includeWorkInProgressEffects && flags & Passive) { + // TODO: Pass `current` as argument to this function + const current = finishedWork.alternate; + commitCachePassiveMountEffect(current, finishedWork); + } + break; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + if (includeWorkInProgressEffects && flags & Passive) { + commitTracingMarkerPassiveMountEffect(finishedWork); + } + break; + } + // Intentional fallthrough to next branch + } + // eslint-disable-next-line-no-fallthrough + default: { + recursivelyTraverseReconnectPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + includeWorkInProgressEffects, + ); + break; + } + } +} + +function recursivelyTraverseAtomicPassiveEffects( + finishedRoot: FiberRoot, + parentFiber: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, +) { + // "Atomic" effects are ones that need to fire on every commit, even during + // pre-rendering. We call this function when traversing a hidden tree whose + // regular effects are currently disconnected. + const prevDebugFiber = getCurrentDebugFiberInDEV(); + // TODO: Add special flag for atomic effects + if (parentFiber.subtreeFlags & PassiveMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + commitAtomicPassiveEffects( + finishedRoot, + child, + committedLanes, + committedTransitions, + ); + child = child.sibling; + } + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +function commitAtomicPassiveEffects( + finishedRoot: FiberRoot, + finishedWork: Fiber, + committedLanes: Lanes, + committedTransitions: Array | null, +) { + // "Atomic" effects are ones that need to fire on every commit, even during + // pre-rendering. We call this function when traversing a hidden tree whose + // regular effects are currently disconnected. + const flags = finishedWork.flags; + switch (finishedWork.tag) { + case OffscreenComponent: { + recursivelyTraverseAtomicPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + if (flags & Passive) { + // TODO: Pass `current` as argument to this function + const current = finishedWork.alternate; + const instance: OffscreenInstance = finishedWork.stateNode; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); + } + break; + } + case CacheComponent: { + recursivelyTraverseAtomicPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + if (flags & Passive) { + // TODO: Pass `current` as argument to this function + const current = finishedWork.alternate; + commitCachePassiveMountEffect(current, finishedWork); + } + break; + } + // eslint-disable-next-line-no-fallthrough + default: { + recursivelyTraverseAtomicPassiveEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + ); + break; + } + } +} + +export function commitPassiveUnmountEffects(finishedWork: Fiber): void { + setCurrentDebugFiberInDEV(finishedWork); + commitPassiveUnmountOnFiber(finishedWork); + resetCurrentDebugFiberInDEV(); +} + +function detachAlternateSiblings(parentFiber: Fiber) { + if (deletedTreeCleanUpLevel >= 1) { + // A fiber was deleted from this parent fiber, but it's still part of the + // previous (alternate) parent fiber's list of children. Because children + // are a linked list, an earlier sibling that's still alive will be + // connected to the deleted fiber via its `alternate`: + // + // live fiber --alternate--> previous live fiber --sibling--> deleted + // fiber + // + // We can't disconnect `alternate` on nodes that haven't been deleted yet, + // but we can disconnect the `sibling` and `child` pointers. + + const previousFiber = parentFiber.alternate; + if (previousFiber !== null) { + let detachedChild = previousFiber.child; + if (detachedChild !== null) { + previousFiber.child = null; + do { + // $FlowFixMe[incompatible-use] found when upgrading Flow + const detachedSibling = detachedChild.sibling; + // $FlowFixMe[incompatible-use] found when upgrading Flow + detachedChild.sibling = null; + detachedChild = detachedSibling; + } while (detachedChild !== null); + } + } + } +} + +function commitHookPassiveUnmountEffects( + finishedWork: Fiber, + nearestMountedAncestor, + hookFlags: HookFlags, +) { + if (shouldProfile(finishedWork)) { + startPassiveEffectTimer(); + commitHookEffectListUnmount( + hookFlags, + finishedWork, + nearestMountedAncestor, + ); + recordPassiveEffectDuration(finishedWork); + } else { + commitHookEffectListUnmount( + hookFlags, + finishedWork, + nearestMountedAncestor, + ); + } +} + +function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void { + // Deletions effects can be scheduled on any fiber type. They need to happen + // before the children effects have fired. + const deletions = parentFiber.deletions; + + if ((parentFiber.flags & ChildDeletion) !== NoFlags) { + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + // TODO: Convert this to use recursion + nextEffect = childToDelete; + commitPassiveUnmountEffectsInsideOfDeletedTree_begin( + childToDelete, + parentFiber, + ); + } + } + detachAlternateSiblings(parentFiber); + } + + const prevDebugFiber = getCurrentDebugFiberInDEV(); + // TODO: Split PassiveMask into separate masks for mount and unmount? + if (parentFiber.subtreeFlags & PassiveMask) { + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + commitPassiveUnmountOnFiber(child); + child = child.sibling; + } + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + recursivelyTraversePassiveUnmountEffects(finishedWork); + if (finishedWork.flags & Passive) { + commitHookPassiveUnmountEffects( + finishedWork, + finishedWork.return, + HookPassive | HookHasEffect, + ); + } + break; + } + case OffscreenComponent: { + const instance: OffscreenInstance = finishedWork.stateNode; + const nextState: OffscreenState | null = finishedWork.memoizedState; + + const isHidden = nextState !== null; + + if ( + isHidden && + instance._visibility & OffscreenPassiveEffectsConnected && + // For backwards compatibility, don't unmount when a tree suspends. In + // the future we may change this to unmount after a delay. + (finishedWork.return === null || + finishedWork.return.tag !== SuspenseComponent) + ) { + // The effects are currently connected. Disconnect them. + // TODO: Add option or heuristic to delay before disconnecting the + // effects. Then if the tree reappears before the delay has elapsed, we + // can skip toggling the effects entirely. + instance._visibility &= ~OffscreenPassiveEffectsConnected; + recursivelyTraverseDisconnectPassiveEffects(finishedWork); + } else { + recursivelyTraversePassiveUnmountEffects(finishedWork); + } + + break; + } + default: { + recursivelyTraversePassiveUnmountEffects(finishedWork); + break; + } + } +} + +function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void { + // Deletions effects can be scheduled on any fiber type. They need to happen + // before the children effects have fired. + const deletions = parentFiber.deletions; + + if ((parentFiber.flags & ChildDeletion) !== NoFlags) { + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + // TODO: Convert this to use recursion + nextEffect = childToDelete; + commitPassiveUnmountEffectsInsideOfDeletedTree_begin( + childToDelete, + parentFiber, + ); + } + } + detachAlternateSiblings(parentFiber); + } + + const prevDebugFiber = getCurrentDebugFiberInDEV(); + // TODO: Check PassiveStatic flag + let child = parentFiber.child; + while (child !== null) { + setCurrentDebugFiberInDEV(child); + disconnectPassiveEffect(child); + child = child.sibling; + } + setCurrentDebugFiberInDEV(prevDebugFiber); +} + +export function disconnectPassiveEffect(finishedWork: Fiber): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + // TODO: Check PassiveStatic flag + commitHookPassiveUnmountEffects( + finishedWork, + finishedWork.return, + HookPassive, + ); + // When disconnecting passive effects, we fire the effects in the same + // order as during a deletiong: parent before child + recursivelyTraverseDisconnectPassiveEffects(finishedWork); + break; + } + case OffscreenComponent: { + const instance: OffscreenInstance = finishedWork.stateNode; + if (instance._visibility & OffscreenPassiveEffectsConnected) { + instance._visibility &= ~OffscreenPassiveEffectsConnected; + recursivelyTraverseDisconnectPassiveEffects(finishedWork); + } else { + // The effects are already disconnected. + } + break; + } + default: { + recursivelyTraverseDisconnectPassiveEffects(finishedWork); + break; + } + } +} + +function commitPassiveUnmountEffectsInsideOfDeletedTree_begin( + deletedSubtreeRoot: Fiber, + nearestMountedAncestor: Fiber | null, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + + // Deletion effects fire in parent -> child order + // TODO: Check if fiber has a PassiveStatic flag + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor); + resetCurrentDebugFiberInDEV(); + + const child = fiber.child; + // TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we + // do this, still need to handle `deletedTreeCleanUpLevel` correctly.) + if (child !== null) { + child.return = fiber; + nextEffect = child; + } else { + commitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot, + ); + } + } +} + +function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + const sibling = fiber.sibling; + const returnFiber = fiber.return; + + if (deletedTreeCleanUpLevel >= 2) { + // Recursively traverse the entire deleted tree and clean up fiber fields. + // This is more aggressive than ideal, and the long term goal is to only + // have to detach the deleted tree at the root. + detachFiberAfterEffects(fiber); + if (fiber === deletedSubtreeRoot) { + nextEffect = null; + return; + } + } else { + // This is the default branch (level 0). We do not recursively clear all + // the fiber fields. Only the root of the deleted subtree. + if (fiber === deletedSubtreeRoot) { + detachFiberAfterEffects(fiber); + nextEffect = null; + return; + } + } + + if (sibling !== null) { + sibling.return = returnFiber; + nextEffect = sibling; + return; + } + + nextEffect = returnFiber; + } +} + +function commitPassiveUnmountInsideDeletedTreeOnFiber( + current: Fiber, + nearestMountedAncestor: Fiber | null, +): void { + switch (current.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + commitHookPassiveUnmountEffects( + current, + nearestMountedAncestor, + HookPassive, + ); + break; + } + // TODO: run passive unmount effects when unmounting a root. + // Because passive unmount effects are not currently run, + // the cache instance owned by the root will never be freed. + // When effects are run, the cache should be freed here: + // case HostRoot: { + // if (enableCache) { + // const cache = current.memoizedState.cache; + // releaseCache(cache); + // } + // break; + // } + case LegacyHiddenComponent: + case OffscreenComponent: { + if (enableCache) { + if ( + current.memoizedState !== null && + current.memoizedState.cachePool !== null + ) { + const cache: Cache = current.memoizedState.cachePool.pool; + // Retain/release the cache used for pending (suspended) nodes. + // Note that this is only reached in the non-suspended/visible case: + // when the content is suspended/hidden, the retain/release occurs + // via the parent Suspense component (see case above). + if (cache != null) { + retainCache(cache); + } + } + } + break; + } + case SuspenseComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const offscreenFiber: Fiber = (current.child: any); + const instance: OffscreenInstance = offscreenFiber.stateNode; + const transitions = instance._transitions; + if (transitions !== null) { + const abortReason = { + reason: 'suspense', + name: current.memoizedProps.unstable_name || null, + }; + if ( + current.memoizedState === null || + current.memoizedState.dehydrated === null + ) { + abortParentMarkerTransitionsForDeletedFiber( + offscreenFiber, + abortReason, + transitions, + instance, + true, + ); + + if (nearestMountedAncestor !== null) { + abortParentMarkerTransitionsForDeletedFiber( + nearestMountedAncestor, + abortReason, + transitions, + instance, + false, + ); + } + } + } + } + break; + } + case CacheComponent: { + if (enableCache) { + const cache = current.memoizedState.cache; + releaseCache(cache); + } + break; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const instance: TracingMarkerInstance = current.stateNode; + const transitions = instance.transitions; + if (transitions !== null) { + const abortReason = { + reason: 'marker', + name: current.memoizedProps.name, + }; + abortParentMarkerTransitionsForDeletedFiber( + current, + abortReason, + transitions, + null, + true, + ); + + if (nearestMountedAncestor !== null) { + abortParentMarkerTransitionsForDeletedFiber( + nearestMountedAncestor, + abortReason, + transitions, + null, + false, + ); + } + } + } + break; + } + } +} + +function invokeLayoutEffectMountInDEV(fiber: Fiber): void { + if (__DEV__) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + try { + commitHookEffectListMount(HookLayout | HookHasEffect, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + break; + } + case ClassComponent: { + const instance = fiber.stateNode; + try { + instance.componentDidMount(); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + break; + } + } + } +} + +function invokePassiveEffectMountInDEV(fiber: Fiber): void { + if (__DEV__) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + try { + commitHookEffectListMount(HookPassive | HookHasEffect, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + break; + } + } + } +} + +function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { + if (__DEV__) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + try { + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + fiber, + fiber.return, + ); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + break; + } + case ClassComponent: { + const instance = fiber.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount(fiber, fiber.return, instance); + } + break; + } + } + } +} + +function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { + if (__DEV__) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + try { + commitHookEffectListUnmount( + HookPassive | HookHasEffect, + fiber, + fiber.return, + ); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + } +} + +export { + commitPlacement, + commitAttachRef, + invokeLayoutEffectMountInDEV, + invokeLayoutEffectUnmountInDEV, + invokePassiveEffectMountInDEV, + invokePassiveEffectUnmountInDEV, +}; From 57a9ba5283d1ea065f0019f7a0b623567d1baea4 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Thu, 20 Oct 2022 16:16:21 +0100 Subject: [PATCH 04/17] Use child instead of child's sibling --- packages/react-reconciler/src/ReactFiberCommitWork.js | 2 +- packages/react-reconciler/src/ReactFiberCommitWork.old.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index dd626c9920a7d..5093abf18aab2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2420,7 +2420,7 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { const _detachOffscreen = () => { instance._visibility |= OffscreenDetached; - let node = currentOffscreenFiber.child?.sibling; + let node = currentOffscreenFiber.child; while (node != null) { disappearLayoutEffects(node); disconnectPassiveEffect(node); diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 04f559edb706e..dbdfa773304fd 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -2420,7 +2420,7 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { const _detachOffscreen = () => { instance._visibility |= OffscreenDetached; - let node = currentOffscreenFiber.child?.sibling; + let node = currentOffscreenFiber.child; while (node != null) { disappearLayoutEffects(node); disconnectPassiveEffect(node); From e26014e8d840c5935fa7db288952ae9496a7df93 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 31 Oct 2022 11:13:24 +0000 Subject: [PATCH 05/17] Nested Offscreen tests --- .../src/__tests__/ReactOffscreen-test.js | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index d21f11f0c1926..67387fae28aa1 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -31,7 +31,24 @@ describe('ReactOffscreen', () => { function Text(props) { Scheduler.unstable_yieldValue(props.text); - return ; + return {props.children}; + } + + function LoggedText({ text, children }) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount ${text}`); + return () => { + Scheduler.unstable_yieldValue(`Unmount ${text}`); + }; + }); + + useLayoutEffect(() => { + Scheduler.unstable_yieldValue(`Mount Layout ${text}`); + return () => { + Scheduler.unstable_yieldValue(`Unmount Layout ${text}`); + }; + }); + return {children}; } // @gate enableLegacyHidden @@ -1825,7 +1842,6 @@ describe('ReactOffscreen', () => { Scheduler.unstable_yieldValue('Unmount Child'); }; }); - useLayoutEffect(() => { Scheduler.unstable_yieldValue('Mount Layout Child'); return () => { @@ -1864,5 +1880,33 @@ describe('ReactOffscreen', () => { expect(spanRef.current).not.toBeNull(); expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']); }); - // TODO: When attach/detach methods are implemented. Add tests for nested Offscreen case. + + // @gate enableOffscreen + fit('something something', async () => { + let outerOffscreen; + let innerOffscreen; + + function App() { + return ( + + (outerOffscreen = el)}> + + (innerOffscreen = el)}> + + + + + + ); + } + + const root = ReactNoop.createRoot(); + + await act(async () => { + root.render(); + }); + + expect(Scheduler).toHaveYielded(['outer', 'Mount Layout outer', 'Mount outer']); + + }); }); From 8c2bc3e3dc916def6e9456861201d6bd7251c10a Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 1 Nov 2022 17:26:57 +0000 Subject: [PATCH 06/17] Add unit tests for nested manual Offscreens --- .../src/ReactFiberCommitWork.js | 5 ++ .../src/ReactFiberCommitWork.old.js | 5 ++ .../src/__tests__/ReactOffscreen-test.js | 79 ++++++++++++++++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 5093abf18aab2..6dc467351305f 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2444,6 +2444,11 @@ export function attachOffscreenInstance(instance: OffscreenInstance): void { ); } + if ((instance._visibility & OffscreenDetached) === NoFlags) { + // The instance is already attached, this is a noop. + return; + } + instance._visibility &= ~OffscreenDetached; const executionContext = getExecutionContext(); diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index dbdfa773304fd..f041ce96e4e91 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -2444,6 +2444,11 @@ export function attachOffscreenInstance(instance: OffscreenInstance): void { ); } + if ((instance._visibility & OffscreenDetached) === NoFlags) { + // The instance is already attached, this is a noop. + return; + } + instance._visibility &= ~OffscreenDetached; const executionContext = getExecutionContext(); diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index 67387fae28aa1..a98754d53b33b 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -34,18 +34,18 @@ describe('ReactOffscreen', () => { return {props.children}; } - function LoggedText({ text, children }) { + function LoggedText({text, children}) { useEffect(() => { - Scheduler.unstable_yieldValue(`Mount ${text}`); + Scheduler.unstable_yieldValue(`mount ${text}`); return () => { - Scheduler.unstable_yieldValue(`Unmount ${text}`); + Scheduler.unstable_yieldValue(`unmount ${text}`); }; }); useLayoutEffect(() => { - Scheduler.unstable_yieldValue(`Mount Layout ${text}`); + Scheduler.unstable_yieldValue(`mount layout ${text}`); return () => { - Scheduler.unstable_yieldValue(`Unmount Layout ${text}`); + Scheduler.unstable_yieldValue(`unmount layout ${text}`); }; }); return {children}; @@ -1875,24 +1875,34 @@ describe('ReactOffscreen', () => { expect(spanRef.current).toBeNull(); expect(Scheduler).toHaveYielded(['Unmount Layout Child', 'Unmount Child']); + // Calling attach on already attached Offscreen. + offscreenRef.detach(); + + expect(Scheduler).toHaveYielded([]); + offscreenRef.attach(); expect(spanRef.current).not.toBeNull(); expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']); + + // Calling attach on already attached Offscreen + offscreenRef.attach(); + + expect(Scheduler).toHaveYielded([]); }); // @gate enableOffscreen - fit('something something', async () => { + it('handles nested manual offscreens', async () => { let outerOffscreen; let innerOffscreen; function App() { return ( - + (outerOffscreen = el)}> - + (innerOffscreen = el)}> - + @@ -1906,7 +1916,56 @@ describe('ReactOffscreen', () => { root.render(); }); - expect(Scheduler).toHaveYielded(['outer', 'Mount Layout outer', 'Mount outer']); + expect(Scheduler).toHaveYielded([ + 'outer', + 'middle', + 'inner', + 'mount layout inner', + 'mount layout middle', + 'mount layout outer', + 'mount inner', + 'mount middle', + 'mount outer', + ]); + + expect(outerOffscreen).not.toBeNull(); + expect(innerOffscreen).not.toBeNull(); + + outerOffscreen.detach(); + expect(innerOffscreen).toBeNull(); + + expect(Scheduler).toHaveYielded([ + 'unmount layout middle', + 'unmount layout inner', + 'unmount middle', + 'unmount inner', + ]); + + outerOffscreen.attach(); + + expect(Scheduler).toHaveYielded([ + 'mount layout inner', + 'mount layout middle', + 'mount inner', + 'mount middle', + ]); + + innerOffscreen.detach(); + + expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']); + + // Calling detach on already detached Offscreen. + innerOffscreen.detach(); + + expect(Scheduler).toHaveYielded([]); + + innerOffscreen.attach(); + + expect(Scheduler).toHaveYielded(['mount layout inner', 'mount inner']); + + innerOffscreen.detach(); + outerOffscreen.attach(); + expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']); }); }); From fd230d9906374e9c7082bf749f526a5eac743a63 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 14 Nov 2022 12:09:16 -0500 Subject: [PATCH 07/17] Tests: Batching multiple attach/detach operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I added some tests to illustrate what should happen when multiple attach/detach operations happen in a single event handler, and the corresponding scenario for when they happen inside an effect. They need to be batched into a single operation — similar to how a setState would work. The simplest case to think about is when you call attach, and then immediately detach on the very next line. This should be a no-op. To implement this properly, we need to queue the operations, like we do for other state updates. --- .../src/__tests__/ReactOffscreen-test.js | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index a98754d53b33b..49f5f46c63c32 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -1968,4 +1968,84 @@ describe('ReactOffscreen', () => { expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']); }); + + // @gate enableOffscreen + it('batches multiple attach and detach calls scheduled from an event handler', async () => { + function Child() { + useEffect(() => { + Scheduler.unstable_yieldValue('Attach child'); + return () => { + Scheduler.unstable_yieldValue('Detach child'); + }; + }, []); + return 'Child'; + } + + const offscreen = React.createRef(null); + function App() { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Attach child']); + + await act(async () => { + const instance = offscreen.current; + + // Attach then immediately re-attach the instance. This should be a + // no-op because the operations are batched. + instance.detach(); + instance.attach(); + }); + // No effects should have attached or detached + expect(Scheduler).toHaveYielded([]); + }); + + // @gate enableOffscreen + it('batches multiple attach and detach calls scheduled from an effect', async () => { + function Child() { + useEffect(() => { + Scheduler.unstable_yieldValue('Attach child'); + return () => { + Scheduler.unstable_yieldValue('Detach child'); + }; + }, []); + return 'Child'; + } + + function App() { + const offscreen = useRef(null); + useLayoutEffect(() => { + const instance = offscreen.current; + + // Attach then immediately re-attach the instance. This should be a + // no-op because the operations are batched. + instance.detach(); + instance.attach(); + }, []); + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded([ + 'Parent effect', + 'Attach child', + + // The child effects should not be toggled + ]); + }); }); From cb4d33a3f385e229b9696b51c80fe0fe094c254a Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 18 Nov 2022 13:42:52 +0000 Subject: [PATCH 08/17] Batch attach/detach calls together --- .../src/ReactFiberCommitWork.js | 50 ++++++++--------- .../src/ReactFiberCommitWork.old.js | 50 ++++++++--------- .../src/ReactFiberOffscreenComponent.js | 1 + .../src/__tests__/ReactOffscreen-test.js | 53 +++++++++++++------ 4 files changed, 84 insertions(+), 70 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 6dc467351305f..5b2effe3b9b26 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -21,7 +21,6 @@ import {NoTimestamp, SyncLane} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; -import {NoLanes} from './ReactFiberLane'; import type {Wakeable} from 'shared/ReactTypes'; import {isOffscreenManual} from './ReactFiberOffscreenComponent'; import type { @@ -171,7 +170,6 @@ import { setIsRunningInsertionEffect, getExecutionContext, CommitContext, - RenderContext, NoContext, } from './ReactFiberWorkLoop'; import { @@ -2411,29 +2409,32 @@ function getRetryCache(finishedWork) { } export function detachOffscreenInstance(instance: OffscreenInstance): void { - const currentOffscreenFiber = instance._current; - if (currentOffscreenFiber === null) { + const fiber = instance._current; + if (fiber === null) { throw new Error( 'Calling Offscreen.detach before instance handle has been set.', ); } - const _detachOffscreen = () => { - instance._visibility |= OffscreenDetached; - let node = currentOffscreenFiber.child; - while (node != null) { - disappearLayoutEffects(node); - disconnectPassiveEffect(node); - node = node.sibling; + if ((instance._visibility & OffscreenDetached) !== NoFlags) { + // The instance is already detached, this is a noop. + return; + } + + instance._pendingVisibility |= OffscreenDetached; + + scheduleMicrotask(() => { + if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { + instance._pendingVisibility = 0; + return; } - }; - const executionContext = getExecutionContext(); - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - scheduleMicrotask(_detachOffscreen); - } else { - _detachOffscreen(); - } + instance._visibility |= OffscreenDetached; + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } + }); } export function attachOffscreenInstance(instance: OffscreenInstance): void { @@ -2444,22 +2445,17 @@ export function attachOffscreenInstance(instance: OffscreenInstance): void { ); } + instance._pendingVisibility &= ~OffscreenDetached; + if ((instance._visibility & OffscreenDetached) === NoFlags) { // The instance is already attached, this is a noop. return; } - instance._visibility &= ~OffscreenDetached; - - const executionContext = getExecutionContext(); const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); - } else { - reappearLayoutEffects(root, fiber.alternate, fiber, false); - reconnectPassiveEffects(root, fiber, NoLanes, null, false); - } + instance._visibility &= ~OffscreenDetached; + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index f041ce96e4e91..afb5bb63c73ee 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -21,7 +21,6 @@ import {NoTimestamp, SyncLane} from './ReactFiberLane.old'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; -import {NoLanes} from './ReactFiberLane.old'; import type {Wakeable} from 'shared/ReactTypes'; import {isOffscreenManual} from './ReactFiberOffscreenComponent'; import type { @@ -171,7 +170,6 @@ import { setIsRunningInsertionEffect, getExecutionContext, CommitContext, - RenderContext, NoContext, } from './ReactFiberWorkLoop.old'; import { @@ -2411,29 +2409,32 @@ function getRetryCache(finishedWork) { } export function detachOffscreenInstance(instance: OffscreenInstance): void { - const currentOffscreenFiber = instance._current; - if (currentOffscreenFiber === null) { + const fiber = instance._current; + if (fiber === null) { throw new Error( 'Calling Offscreen.detach before instance handle has been set.', ); } - const _detachOffscreen = () => { - instance._visibility |= OffscreenDetached; - let node = currentOffscreenFiber.child; - while (node != null) { - disappearLayoutEffects(node); - disconnectPassiveEffect(node); - node = node.sibling; + if ((instance._visibility & OffscreenDetached) !== NoFlags) { + // The instance is already detached, this is a noop. + return; + } + + instance._pendingVisibility |= OffscreenDetached; + + scheduleMicrotask(() => { + if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { + instance._pendingVisibility = 0; + return; } - }; - const executionContext = getExecutionContext(); - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - scheduleMicrotask(_detachOffscreen); - } else { - _detachOffscreen(); - } + instance._visibility |= OffscreenDetached; + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } + }); } export function attachOffscreenInstance(instance: OffscreenInstance): void { @@ -2444,22 +2445,17 @@ export function attachOffscreenInstance(instance: OffscreenInstance): void { ); } + instance._pendingVisibility &= ~OffscreenDetached; + if ((instance._visibility & OffscreenDetached) === NoFlags) { // The instance is already attached, this is a noop. return; } - instance._visibility &= ~OffscreenDetached; - - const executionContext = getExecutionContext(); const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); - } else { - reappearLayoutEffects(root, fiber.alternate, fiber, false); - reconnectPassiveEffects(root, fiber, NoLanes, null, false); - } + instance._visibility &= ~OffscreenDetached; + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); } } diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index 6c68dfac53a48..d8833779195d1 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -50,6 +50,7 @@ export const OffscreenDetached = /* */ 0b010; export const OffscreenPassiveEffectsConnected = /* */ 0b100; export type OffscreenInstance = { + _pendingVisibility: OffscreenVisibility, _visibility: OffscreenVisibility, _pendingMarkers: Set | null, _transitions: Set | null, diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index 49f5f46c63c32..58be644c43127 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -1554,7 +1554,9 @@ describe('ReactOffscreen', () => { ); }); - offscreenRef.current.detach(); + await act(async () => { + offscreenRef.current.detach(); + }); // Offscreen is detached. State updates from offscreen are **defered**. await act(async () => { @@ -1577,7 +1579,9 @@ describe('ReactOffscreen', () => { , ); - offscreenRef.current.attach(); + await act(async () => { + offscreenRef.current.attach(); + }); // Offscreen is attached. State updates from offscreen are **not defered**. await act(async () => { @@ -1617,9 +1621,9 @@ describe('ReactOffscreen', () => { const text = 'HighPriorityComponent ' + state; useLayoutEffect(() => { if (nextRenderTriggerDetach) { - offscreenRef.current.detach(); _stateUpdate(state + 1); updateChildState(state + 1); + offscreenRef.current.detach(); nextRenderTriggerDetach = false; } @@ -1870,17 +1874,23 @@ describe('ReactOffscreen', () => { expect(spanRef.current).not.toBeNull(); expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']); - offscreenRef.detach(); + await act(async () => { + offscreenRef.detach(); + }); expect(spanRef.current).toBeNull(); expect(Scheduler).toHaveYielded(['Unmount Layout Child', 'Unmount Child']); // Calling attach on already attached Offscreen. - offscreenRef.detach(); + await act(async () => { + offscreenRef.detach(); + }); expect(Scheduler).toHaveYielded([]); - offscreenRef.attach(); + await act(async () => { + offscreenRef.attach(); + }); expect(spanRef.current).not.toBeNull(); expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']); @@ -1931,7 +1941,10 @@ describe('ReactOffscreen', () => { expect(outerOffscreen).not.toBeNull(); expect(innerOffscreen).not.toBeNull(); - outerOffscreen.detach(); + await act(async () => { + outerOffscreen.detach(); + }); + expect(innerOffscreen).toBeNull(); expect(Scheduler).toHaveYielded([ @@ -1941,7 +1954,9 @@ describe('ReactOffscreen', () => { 'unmount inner', ]); - outerOffscreen.attach(); + await act(async () => { + outerOffscreen.attach(); + }); expect(Scheduler).toHaveYielded([ 'mount layout inner', @@ -1950,21 +1965,29 @@ describe('ReactOffscreen', () => { 'mount middle', ]); - innerOffscreen.detach(); + await act(async () => { + innerOffscreen.detach(); + }); expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']); // Calling detach on already detached Offscreen. - innerOffscreen.detach(); + await act(async () => { + innerOffscreen.detach(); + }); expect(Scheduler).toHaveYielded([]); - innerOffscreen.attach(); + await act(async () => { + innerOffscreen.attach(); + }); expect(Scheduler).toHaveYielded(['mount layout inner', 'mount inner']); - innerOffscreen.detach(); - outerOffscreen.attach(); + await act(async () => { + innerOffscreen.detach(); + outerOffscreen.attach(); + }); expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']); }); @@ -1994,6 +2017,7 @@ describe('ReactOffscreen', () => { await act(() => { root.render(); }); + expect(Scheduler).toHaveYielded(['Attach child']); await act(async () => { @@ -2042,10 +2066,7 @@ describe('ReactOffscreen', () => { root.render(); }); expect(Scheduler).toHaveYielded([ - 'Parent effect', 'Attach child', - - // The child effects should not be toggled ]); }); }); From ce0c0305e98dbe4090e7d7110bae120968cc4d5a Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 18 Nov 2022 14:07:06 +0000 Subject: [PATCH 09/17] Clean up tests + add comments --- packages/react-reconciler/src/ReactFiber.js | 2 + .../react-reconciler/src/ReactFiber.old.js | 917 ++++++++++++++++++ .../src/ReactFiberCommitWork.js | 3 +- .../src/ReactFiberCommitWork.old.js | 3 +- .../src/__tests__/ReactOffscreen-test.js | 42 +- 5 files changed, 950 insertions(+), 17 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiber.old.js diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 31d7deacb5f30..f1e473ebf0b98 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -753,6 +753,7 @@ export function createFiberFromOffscreen( fiber.lanes = lanes; const primaryChildInstance: OffscreenInstance = { _visibility: OffscreenVisible, + _pendingVisibility: OffscreenVisible, _pendingMarkers: null, _retryCache: null, _transitions: null, @@ -777,6 +778,7 @@ export function createFiberFromLegacyHidden( // the offscreen implementation, which depends on a state node const instance: OffscreenInstance = { _visibility: OffscreenVisible, + _pendingVisibility: OffscreenVisible, _pendingMarkers: null, _transitions: null, _retryCache: null, diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js new file mode 100644 index 0000000000000..3b3b9b7d259cb --- /dev/null +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -0,0 +1,917 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactElement} from 'shared/ReactElementType'; +import type {ReactFragment, ReactPortal, ReactScope} from 'shared/ReactTypes'; +import type {Fiber} from './ReactInternalTypes'; +import type {RootTag} from './ReactRootTags'; +import type {WorkTag} from './ReactWorkTags'; +import type {TypeOfMode} from './ReactTypeOfMode'; +import type {Lanes} from './ReactFiberLane.old'; +import type {SuspenseInstance} from './ReactFiberHostConfig'; +import type { + OffscreenProps, + OffscreenInstance, +} from './ReactFiberOffscreenComponent'; +import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old'; + +import { + supportsResources, + supportsSingletons, + isHostResourceType, + isHostSingletonType, +} from './ReactFiberHostConfig'; +import { + createRootStrictEffectsByDefault, + enableCache, + enableProfilerTimer, + enableScopeAPI, + enableLegacyHidden, + enableSyncDefaultUpdates, + allowConcurrentByDefault, + enableTransitionTracing, + enableDebugTracing, + enableFloat, + enableHostSingletons, +} from 'shared/ReactFeatureFlags'; +import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; +import {ConcurrentRoot} from './ReactRootTags'; +import { + IndeterminateComponent, + ClassComponent, + HostRoot, + HostComponent, + HostText, + HostPortal, + HostResource, + HostSingleton, + ForwardRef, + Fragment, + Mode, + ContextProvider, + ContextConsumer, + Profiler, + SuspenseComponent, + SuspenseListComponent, + DehydratedFragment, + FunctionComponent, + MemoComponent, + SimpleMemoComponent, + LazyComponent, + ScopeComponent, + OffscreenComponent, + LegacyHiddenComponent, + CacheComponent, + TracingMarkerComponent, +} from './ReactWorkTags'; +import {OffscreenVisible} from './ReactFiberOffscreenComponent'; +import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; +import { + resolveClassForHotReloading, + resolveFunctionForHotReloading, + resolveForwardRefForHotReloading, +} from './ReactFiberHotReloading.old'; +import {NoLanes} from './ReactFiberLane.old'; +import { + NoMode, + ConcurrentMode, + DebugTracingMode, + ProfileMode, + StrictLegacyMode, + StrictEffectsMode, + ConcurrentUpdatesByDefaultMode, +} from './ReactTypeOfMode'; +import { + REACT_FORWARD_REF_TYPE, + REACT_FRAGMENT_TYPE, + REACT_DEBUG_TRACING_MODE_TYPE, + REACT_STRICT_MODE_TYPE, + REACT_PROFILER_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONTEXT_TYPE, + REACT_SUSPENSE_TYPE, + REACT_SUSPENSE_LIST_TYPE, + REACT_MEMO_TYPE, + REACT_LAZY_TYPE, + REACT_SCOPE_TYPE, + REACT_OFFSCREEN_TYPE, + REACT_LEGACY_HIDDEN_TYPE, + REACT_CACHE_TYPE, + REACT_TRACING_MARKER_TYPE, +} from 'shared/ReactSymbols'; +import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old'; +import { + detachOffscreenInstance, + attachOffscreenInstance, +} from './ReactFiberCommitWork.old'; +import {getHostContext} from './ReactFiberHostContext.old'; + +export type {Fiber}; + +let hasBadMapPolyfill; + +if (__DEV__) { + hasBadMapPolyfill = false; + try { + const nonExtensibleObject = Object.preventExtensions({}); + /* eslint-disable no-new */ + new Map([[nonExtensibleObject, null]]); + new Set([nonExtensibleObject]); + /* eslint-enable no-new */ + } catch (e) { + // TODO: Consider warning about bad polyfills + hasBadMapPolyfill = true; + } +} + +function FiberNode( + tag: WorkTag, + pendingProps: mixed, + key: null | string, + mode: TypeOfMode, +) { + // Instance + this.tag = tag; + this.key = key; + this.elementType = null; + this.type = null; + this.stateNode = null; + + // Fiber + this.return = null; + this.child = null; + this.sibling = null; + this.index = 0; + + this.ref = null; + this.refCleanup = null; + + this.pendingProps = pendingProps; + this.memoizedProps = null; + this.updateQueue = null; + this.memoizedState = null; + this.dependencies = null; + + this.mode = mode; + + // Effects + this.flags = NoFlags; + this.subtreeFlags = NoFlags; + this.deletions = null; + + this.lanes = NoLanes; + this.childLanes = NoLanes; + + this.alternate = null; + + if (enableProfilerTimer) { + // Note: The following is done to avoid a v8 performance cliff. + // + // Initializing the fields below to smis and later updating them with + // double values will cause Fibers to end up having separate shapes. + // This behavior/bug has something to do with Object.preventExtension(). + // Fortunately this only impacts DEV builds. + // Unfortunately it makes React unusably slow for some applications. + // To work around this, initialize the fields below with doubles. + // + // Learn more about this here: + // https://github.com/facebook/react/issues/14365 + // https://bugs.chromium.org/p/v8/issues/detail?id=8538 + this.actualDuration = Number.NaN; + this.actualStartTime = Number.NaN; + this.selfBaseDuration = Number.NaN; + this.treeBaseDuration = Number.NaN; + + // It's okay to replace the initial doubles with smis after initialization. + // This won't trigger the performance cliff mentioned above, + // and it simplifies other profiler code (including DevTools). + this.actualDuration = 0; + this.actualStartTime = -1; + this.selfBaseDuration = 0; + this.treeBaseDuration = 0; + } + + if (__DEV__) { + // This isn't directly used but is handy for debugging internals: + + this._debugSource = null; + this._debugOwner = null; + this._debugNeedsRemount = false; + this._debugHookTypes = null; + if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') { + Object.preventExtensions(this); + } + } +} + +// This is a constructor function, rather than a POJO constructor, still +// please ensure we do the following: +// 1) Nobody should add any instance methods on this. Instance methods can be +// more difficult to predict when they get optimized and they are almost +// never inlined properly in static compilers. +// 2) Nobody should rely on `instanceof Fiber` for type testing. We should +// always know when it is a fiber. +// 3) We might want to experiment with using numeric keys since they are easier +// to optimize in a non-JIT environment. +// 4) We can easily go from a constructor to a createFiber object literal if that +// is faster. +// 5) It should be easy to port this to a C struct and keep a C implementation +// compatible. +const createFiber = function( + tag: WorkTag, + pendingProps: mixed, + key: null | string, + mode: TypeOfMode, +): Fiber { + // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors + return new FiberNode(tag, pendingProps, key, mode); +}; + +function shouldConstruct(Component: Function) { + const prototype = Component.prototype; + return !!(prototype && prototype.isReactComponent); +} + +export function isSimpleFunctionComponent(type: any): boolean { + return ( + typeof type === 'function' && + !shouldConstruct(type) && + type.defaultProps === undefined + ); +} + +export function resolveLazyComponentTag(Component: Function): WorkTag { + if (typeof Component === 'function') { + return shouldConstruct(Component) ? ClassComponent : FunctionComponent; + } else if (Component !== undefined && Component !== null) { + const $$typeof = Component.$$typeof; + if ($$typeof === REACT_FORWARD_REF_TYPE) { + return ForwardRef; + } + if ($$typeof === REACT_MEMO_TYPE) { + return MemoComponent; + } + } + return IndeterminateComponent; +} + +// This is used to create an alternate fiber to do work on. +export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { + let workInProgress = current.alternate; + if (workInProgress === null) { + // We use a double buffering pooling technique because we know that we'll + // only ever need at most two versions of a tree. We pool the "other" unused + // node that we're free to reuse. This is lazily created to avoid allocating + // extra objects for things that are never updated. It also allow us to + // reclaim the extra memory if needed. + workInProgress = createFiber( + current.tag, + pendingProps, + current.key, + current.mode, + ); + workInProgress.elementType = current.elementType; + workInProgress.type = current.type; + workInProgress.stateNode = current.stateNode; + + if (__DEV__) { + // DEV-only fields + + workInProgress._debugSource = current._debugSource; + workInProgress._debugOwner = current._debugOwner; + workInProgress._debugHookTypes = current._debugHookTypes; + } + + workInProgress.alternate = current; + current.alternate = workInProgress; + } else { + workInProgress.pendingProps = pendingProps; + // Needed because Blocks store data on type. + workInProgress.type = current.type; + + // We already have an alternate. + // Reset the effect tag. + workInProgress.flags = NoFlags; + + // The effects are no longer valid. + workInProgress.subtreeFlags = NoFlags; + workInProgress.deletions = null; + + if (enableProfilerTimer) { + // We intentionally reset, rather than copy, actualDuration & actualStartTime. + // This prevents time from endlessly accumulating in new commits. + // This has the downside of resetting values for different priority renders, + // But works for yielding (the common case) and should support resuming. + workInProgress.actualDuration = 0; + workInProgress.actualStartTime = -1; + } + } + + // Reset all effects except static ones. + // Static effects are not specific to a render. + workInProgress.flags = current.flags & StaticMask; + workInProgress.childLanes = current.childLanes; + workInProgress.lanes = current.lanes; + + workInProgress.child = current.child; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.memoizedState = current.memoizedState; + workInProgress.updateQueue = current.updateQueue; + + // Clone the dependencies object. This is mutated during the render phase, so + // it cannot be shared with the current fiber. + const currentDependencies = current.dependencies; + workInProgress.dependencies = + currentDependencies === null + ? null + : { + lanes: currentDependencies.lanes, + firstContext: currentDependencies.firstContext, + }; + + // These will be overridden during the parent's reconciliation + workInProgress.sibling = current.sibling; + workInProgress.index = current.index; + workInProgress.ref = current.ref; + workInProgress.refCleanup = current.refCleanup; + + if (enableProfilerTimer) { + workInProgress.selfBaseDuration = current.selfBaseDuration; + workInProgress.treeBaseDuration = current.treeBaseDuration; + } + + if (__DEV__) { + workInProgress._debugNeedsRemount = current._debugNeedsRemount; + switch (workInProgress.tag) { + case IndeterminateComponent: + case FunctionComponent: + case SimpleMemoComponent: + workInProgress.type = resolveFunctionForHotReloading(current.type); + break; + case ClassComponent: + workInProgress.type = resolveClassForHotReloading(current.type); + break; + case ForwardRef: + workInProgress.type = resolveForwardRefForHotReloading(current.type); + break; + default: + break; + } + } + + return workInProgress; +} + +// Used to reuse a Fiber for a second pass. +export function resetWorkInProgress( + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber { + // This resets the Fiber to what createFiber or createWorkInProgress would + // have set the values to before during the first pass. Ideally this wouldn't + // be necessary but unfortunately many code paths reads from the workInProgress + // when they should be reading from current and writing to workInProgress. + + // We assume pendingProps, index, key, ref, return are still untouched to + // avoid doing another reconciliation. + + // Reset the effect flags but keep any Placement tags, since that's something + // that child fiber is setting, not the reconciliation. + workInProgress.flags &= StaticMask | Placement; + + // The effects are no longer valid. + + const current = workInProgress.alternate; + if (current === null) { + // Reset to createFiber's initial values. + workInProgress.childLanes = NoLanes; + workInProgress.lanes = renderLanes; + + workInProgress.child = null; + workInProgress.subtreeFlags = NoFlags; + workInProgress.memoizedProps = null; + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + + workInProgress.dependencies = null; + + workInProgress.stateNode = null; + + if (enableProfilerTimer) { + // Note: We don't reset the actualTime counts. It's useful to accumulate + // actual time across multiple render passes. + workInProgress.selfBaseDuration = 0; + workInProgress.treeBaseDuration = 0; + } + } else { + // Reset to the cloned values that createWorkInProgress would've. + workInProgress.childLanes = current.childLanes; + workInProgress.lanes = current.lanes; + + workInProgress.child = current.child; + workInProgress.subtreeFlags = NoFlags; + workInProgress.deletions = null; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.memoizedState = current.memoizedState; + workInProgress.updateQueue = current.updateQueue; + // Needed because Blocks store data on type. + workInProgress.type = current.type; + + // Clone the dependencies object. This is mutated during the render phase, so + // it cannot be shared with the current fiber. + const currentDependencies = current.dependencies; + workInProgress.dependencies = + currentDependencies === null + ? null + : { + lanes: currentDependencies.lanes, + firstContext: currentDependencies.firstContext, + }; + + if (enableProfilerTimer) { + // Note: We don't reset the actualTime counts. It's useful to accumulate + // actual time across multiple render passes. + workInProgress.selfBaseDuration = current.selfBaseDuration; + workInProgress.treeBaseDuration = current.treeBaseDuration; + } + } + + return workInProgress; +} + +export function createHostRootFiber( + tag: RootTag, + isStrictMode: boolean, + concurrentUpdatesByDefaultOverride: null | boolean, +): Fiber { + let mode; + if (tag === ConcurrentRoot) { + mode = ConcurrentMode; + if (isStrictMode === true || createRootStrictEffectsByDefault) { + mode |= StrictLegacyMode | StrictEffectsMode; + } + if ( + // We only use this flag for our repo tests to check both behaviors. + // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting" + !enableSyncDefaultUpdates || + // Only for internal experiments. + (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride) + ) { + mode |= ConcurrentUpdatesByDefaultMode; + } + } else { + mode = NoMode; + } + + if (enableProfilerTimer && isDevToolsPresent) { + // Always collect profile timings when DevTools are present. + // This enables DevTools to start capturing timing at any point– + // Without some nodes in the tree having empty base times. + mode |= ProfileMode; + } + + return createFiber(HostRoot, null, null, mode); +} + +export function createFiberFromTypeAndProps( + type: any, // React$ElementType + key: null | string, + pendingProps: any, + owner: null | Fiber, + mode: TypeOfMode, + lanes: Lanes, +): Fiber { + let fiberTag = IndeterminateComponent; + // The resolved type is set if we know what the final type will be. I.e. it's not lazy. + let resolvedType = type; + if (typeof type === 'function') { + if (shouldConstruct(type)) { + fiberTag = ClassComponent; + if (__DEV__) { + resolvedType = resolveClassForHotReloading(resolvedType); + } + } else { + if (__DEV__) { + resolvedType = resolveFunctionForHotReloading(resolvedType); + } + } + } else if (typeof type === 'string') { + if ( + enableFloat && + supportsResources && + enableHostSingletons && + supportsSingletons + ) { + const hostContext = getHostContext(); + fiberTag = isHostResourceType(type, pendingProps, hostContext) + ? HostResource + : isHostSingletonType(type) + ? HostSingleton + : HostComponent; + } else if (enableFloat && supportsResources) { + const hostContext = getHostContext(); + fiberTag = isHostResourceType(type, pendingProps, hostContext) + ? HostResource + : HostComponent; + } else if (enableHostSingletons && supportsSingletons) { + fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent; + } else { + fiberTag = HostComponent; + } + } else { + getTag: switch (type) { + case REACT_FRAGMENT_TYPE: + return createFiberFromFragment(pendingProps.children, mode, lanes, key); + case REACT_STRICT_MODE_TYPE: + fiberTag = Mode; + mode |= StrictLegacyMode; + if ((mode & ConcurrentMode) !== NoMode) { + // Strict effects should never run on legacy roots + mode |= StrictEffectsMode; + } + break; + case REACT_PROFILER_TYPE: + return createFiberFromProfiler(pendingProps, mode, lanes, key); + case REACT_SUSPENSE_TYPE: + return createFiberFromSuspense(pendingProps, mode, lanes, key); + case REACT_SUSPENSE_LIST_TYPE: + return createFiberFromSuspenseList(pendingProps, mode, lanes, key); + case REACT_OFFSCREEN_TYPE: + return createFiberFromOffscreen(pendingProps, mode, lanes, key); + case REACT_LEGACY_HIDDEN_TYPE: + if (enableLegacyHidden) { + return createFiberFromLegacyHidden(pendingProps, mode, lanes, key); + } + // eslint-disable-next-line no-fallthrough + case REACT_SCOPE_TYPE: + if (enableScopeAPI) { + return createFiberFromScope(type, pendingProps, mode, lanes, key); + } + // eslint-disable-next-line no-fallthrough + case REACT_CACHE_TYPE: + if (enableCache) { + return createFiberFromCache(pendingProps, mode, lanes, key); + } + // eslint-disable-next-line no-fallthrough + case REACT_TRACING_MARKER_TYPE: + if (enableTransitionTracing) { + return createFiberFromTracingMarker(pendingProps, mode, lanes, key); + } + // eslint-disable-next-line no-fallthrough + case REACT_DEBUG_TRACING_MODE_TYPE: + if (enableDebugTracing) { + fiberTag = Mode; + mode |= DebugTracingMode; + break; + } + // eslint-disable-next-line no-fallthrough + default: { + if (typeof type === 'object' && type !== null) { + switch (type.$$typeof) { + case REACT_PROVIDER_TYPE: + fiberTag = ContextProvider; + break getTag; + case REACT_CONTEXT_TYPE: + // This is a consumer + fiberTag = ContextConsumer; + break getTag; + case REACT_FORWARD_REF_TYPE: + fiberTag = ForwardRef; + if (__DEV__) { + resolvedType = resolveForwardRefForHotReloading(resolvedType); + } + break getTag; + case REACT_MEMO_TYPE: + fiberTag = MemoComponent; + break getTag; + case REACT_LAZY_TYPE: + fiberTag = LazyComponent; + resolvedType = null; + break getTag; + } + } + let info = ''; + if (__DEV__) { + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in, or you might have mixed up default and " + + 'named imports.'; + } + const ownerName = owner ? getComponentNameFromFiber(owner) : null; + if (ownerName) { + info += '\n\nCheck the render method of `' + ownerName + '`.'; + } + } + + throw new Error( + 'Element type is invalid: expected a string (for built-in ' + + 'components) or a class/function (for composite components) ' + + `but got: ${type == null ? type : typeof type}.${info}`, + ); + } + } + } + + const fiber = createFiber(fiberTag, pendingProps, key, mode); + fiber.elementType = type; + fiber.type = resolvedType; + fiber.lanes = lanes; + + if (__DEV__) { + fiber._debugOwner = owner; + } + + return fiber; +} + +export function createFiberFromElement( + element: ReactElement, + mode: TypeOfMode, + lanes: Lanes, +): Fiber { + let owner = null; + if (__DEV__) { + owner = element._owner; + } + const type = element.type; + const key = element.key; + const pendingProps = element.props; + const fiber = createFiberFromTypeAndProps( + type, + key, + pendingProps, + owner, + mode, + lanes, + ); + if (__DEV__) { + fiber._debugSource = element._source; + fiber._debugOwner = element._owner; + } + return fiber; +} + +export function createFiberFromFragment( + elements: ReactFragment, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(Fragment, elements, key, mode); + fiber.lanes = lanes; + return fiber; +} + +function createFiberFromScope( + scope: ReactScope, + pendingProps: any, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +) { + const fiber = createFiber(ScopeComponent, pendingProps, key, mode); + fiber.type = scope; + fiber.elementType = scope; + fiber.lanes = lanes; + return fiber; +} + +function createFiberFromProfiler( + pendingProps: any, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + if (__DEV__) { + if (typeof pendingProps.id !== 'string') { + console.error( + 'Profiler must specify an "id" of type `string` as a prop. Received the type `%s` instead.', + typeof pendingProps.id, + ); + } + } + + const fiber = createFiber(Profiler, pendingProps, key, mode | ProfileMode); + fiber.elementType = REACT_PROFILER_TYPE; + fiber.lanes = lanes; + + if (enableProfilerTimer) { + fiber.stateNode = { + effectDuration: 0, + passiveEffectDuration: 0, + }; + } + + return fiber; +} + +export function createFiberFromSuspense( + pendingProps: any, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(SuspenseComponent, pendingProps, key, mode); + fiber.elementType = REACT_SUSPENSE_TYPE; + fiber.lanes = lanes; + return fiber; +} + +export function createFiberFromSuspenseList( + pendingProps: any, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode); + fiber.elementType = REACT_SUSPENSE_LIST_TYPE; + fiber.lanes = lanes; + return fiber; +} + +export function createFiberFromOffscreen( + pendingProps: OffscreenProps, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(OffscreenComponent, pendingProps, key, mode); + fiber.elementType = REACT_OFFSCREEN_TYPE; + fiber.lanes = lanes; + const primaryChildInstance: OffscreenInstance = { + _visibility: OffscreenVisible, + _pendingVisibility: OffscreenVisible, + _pendingMarkers: null, + _retryCache: null, + _transitions: null, + _current: null, + detach: () => detachOffscreenInstance(primaryChildInstance), + attach: () => attachOffscreenInstance(primaryChildInstance), + }; + fiber.stateNode = primaryChildInstance; + return fiber; +} + +export function createFiberFromLegacyHidden( + pendingProps: OffscreenProps, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode); + fiber.elementType = REACT_LEGACY_HIDDEN_TYPE; + fiber.lanes = lanes; + // Adding a stateNode for legacy hidden because it's currently using + // the offscreen implementation, which depends on a state node + const instance: OffscreenInstance = { + _visibility: OffscreenVisible, + _pendingVisibility: OffscreenVisible, + _pendingMarkers: null, + _transitions: null, + _retryCache: null, + _current: null, + detach: () => detachOffscreenInstance(instance), + attach: () => attachOffscreenInstance(instance), + }; + fiber.stateNode = instance; + return fiber; +} + +export function createFiberFromCache( + pendingProps: any, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(CacheComponent, pendingProps, key, mode); + fiber.elementType = REACT_CACHE_TYPE; + fiber.lanes = lanes; + return fiber; +} + +export function createFiberFromTracingMarker( + pendingProps: any, + mode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + const fiber = createFiber(TracingMarkerComponent, pendingProps, key, mode); + fiber.elementType = REACT_TRACING_MARKER_TYPE; + fiber.lanes = lanes; + const tracingMarkerInstance: TracingMarkerInstance = { + tag: TransitionTracingMarker, + transitions: null, + pendingBoundaries: null, + aborts: null, + name: pendingProps.name, + }; + fiber.stateNode = tracingMarkerInstance; + return fiber; +} + +export function createFiberFromText( + content: string, + mode: TypeOfMode, + lanes: Lanes, +): Fiber { + const fiber = createFiber(HostText, content, null, mode); + fiber.lanes = lanes; + return fiber; +} + +export function createFiberFromHostInstanceForDeletion(): Fiber { + const fiber = createFiber(HostComponent, null, null, NoMode); + fiber.elementType = 'DELETED'; + return fiber; +} + +export function createFiberFromDehydratedFragment( + dehydratedNode: SuspenseInstance, +): Fiber { + const fiber = createFiber(DehydratedFragment, null, null, NoMode); + fiber.stateNode = dehydratedNode; + return fiber; +} + +export function createFiberFromPortal( + portal: ReactPortal, + mode: TypeOfMode, + lanes: Lanes, +): Fiber { + const pendingProps = portal.children !== null ? portal.children : []; + const fiber = createFiber(HostPortal, pendingProps, portal.key, mode); + fiber.lanes = lanes; + fiber.stateNode = { + containerInfo: portal.containerInfo, + pendingChildren: null, // Used by persistent updates + implementation: portal.implementation, + }; + return fiber; +} + +// Used for stashing WIP properties to replay failed work in DEV. +export function assignFiberPropertiesInDEV( + target: Fiber | null, + source: Fiber, +): Fiber { + if (target === null) { + // This Fiber's initial properties will always be overwritten. + // We only use a Fiber to ensure the same hidden class so DEV isn't slow. + target = createFiber(IndeterminateComponent, null, null, NoMode); + } + + // This is intentionally written as a list of all properties. + // We tried to use Object.assign() instead but this is called in + // the hottest path, and Object.assign() was too slow: + // https://github.com/facebook/react/issues/12502 + // This code is DEV-only so size is not a concern. + + target.tag = source.tag; + target.key = source.key; + target.elementType = source.elementType; + target.type = source.type; + target.stateNode = source.stateNode; + target.return = source.return; + target.child = source.child; + target.sibling = source.sibling; + target.index = source.index; + target.ref = source.ref; + target.refCleanup = source.refCleanup; + target.pendingProps = source.pendingProps; + target.memoizedProps = source.memoizedProps; + target.updateQueue = source.updateQueue; + target.memoizedState = source.memoizedState; + target.dependencies = source.dependencies; + target.mode = source.mode; + target.flags = source.flags; + target.subtreeFlags = source.subtreeFlags; + target.deletions = source.deletions; + target.lanes = source.lanes; + target.childLanes = source.childLanes; + target.alternate = source.alternate; + if (enableProfilerTimer) { + target.actualDuration = source.actualDuration; + target.actualStartTime = source.actualStartTime; + target.selfBaseDuration = source.selfBaseDuration; + target.treeBaseDuration = source.treeBaseDuration; + } + + target._debugSource = source._debugSource; + target._debugOwner = source._debugOwner; + target._debugNeedsRemount = source._debugNeedsRemount; + target._debugHookTypes = source._debugHookTypes; + return target; +} diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 5b2effe3b9b26..11bd73b607a27 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2423,9 +2423,10 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { instance._pendingVisibility |= OffscreenDetached; + // Detaching needs to be postoned in case attach is called before next update. scheduleMicrotask(() => { if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { - instance._pendingVisibility = 0; + // Attach was called. Offscreen does not need to be detached. return; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index afb5bb63c73ee..c879678d27e57 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -2423,9 +2423,10 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { instance._pendingVisibility |= OffscreenDetached; + // Detaching needs to be postoned in case attach is called before next update. scheduleMicrotask(() => { if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { - instance._pendingVisibility = 0; + // Attach was called. Offscreen does not need to be detached. return; } diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index 58be644c43127..ea19c20de90ed 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -1996,12 +1996,12 @@ describe('ReactOffscreen', () => { it('batches multiple attach and detach calls scheduled from an event handler', async () => { function Child() { useEffect(() => { - Scheduler.unstable_yieldValue('Attach child'); + Scheduler.unstable_yieldValue('attach child'); return () => { - Scheduler.unstable_yieldValue('Detach child'); + Scheduler.unstable_yieldValue('detach child'); }; }, []); - return 'Child'; + return 'child'; } const offscreen = React.createRef(null); @@ -2018,39 +2018,51 @@ describe('ReactOffscreen', () => { root.render(); }); - expect(Scheduler).toHaveYielded(['Attach child']); + expect(Scheduler).toHaveYielded(['attach child']); await act(async () => { const instance = offscreen.current; - - // Attach then immediately re-attach the instance. This should be a - // no-op because the operations are batched. + // Detach then immediately attach the instance. instance.detach(); instance.attach(); }); - // No effects should have attached or detached + expect(Scheduler).toHaveYielded([]); + + await act(async () => { + const instance = offscreen.current; + instance.detach(); + }); + + expect(Scheduler).toHaveYielded(['detach child']); + + await act(async () => { + const instance = offscreen.current; + // Attach then immediately detach. + instance.attach(); + instance.detach(); + }); + + expect(Scheduler).toHaveYielded(['attach child', 'detach child']); }); // @gate enableOffscreen it('batches multiple attach and detach calls scheduled from an effect', async () => { function Child() { useEffect(() => { - Scheduler.unstable_yieldValue('Attach child'); + Scheduler.unstable_yieldValue('attach child'); return () => { - Scheduler.unstable_yieldValue('Detach child'); + Scheduler.unstable_yieldValue('detach child'); }; }, []); - return 'Child'; + return 'child'; } function App() { const offscreen = useRef(null); useLayoutEffect(() => { const instance = offscreen.current; - - // Attach then immediately re-attach the instance. This should be a - // no-op because the operations are batched. + // Detach then immediately attach the instance. instance.detach(); instance.attach(); }, []); @@ -2066,7 +2078,7 @@ describe('ReactOffscreen', () => { root.render(); }); expect(Scheduler).toHaveYielded([ - 'Attach child', + 'attach child' ]); }); }); From 40530f06be601c54fb4afe69aeae997eeb8505a0 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 18 Nov 2022 14:27:50 +0000 Subject: [PATCH 10/17] Prettier --- .../react-reconciler/src/__tests__/ReactOffscreen-test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index ea19c20de90ed..c44f261337fcc 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -2077,8 +2077,6 @@ describe('ReactOffscreen', () => { await act(() => { root.render(); }); - expect(Scheduler).toHaveYielded([ - 'attach child' - ]); + expect(Scheduler).toHaveYielded(['attach child']); }); }); From 601bf5ea33162280955c96cad77bc8c3226590b6 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 21 Nov 2022 13:30:37 +0000 Subject: [PATCH 11/17] Unify use of _pendingVisibility in attach/detach --- .../src/ReactFiberBeginWork.js | 6 + .../src/ReactFiberBeginWork.old.js | 4218 +++++++++++++++++ .../src/ReactFiberCommitWork.js | 29 +- .../src/ReactFiberCommitWork.old.js | 29 +- .../src/__tests__/ReactOffscreen-test.js | 40 +- 5 files changed, 4253 insertions(+), 69 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberBeginWork.old.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 78372d1762916..38bec8b696e36 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -681,6 +681,12 @@ function updateOffscreenComponent( const prevState: OffscreenState | null = current !== null ? current.memoizedState : null; + // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is + // to support batching of `attach` and `detach` calls. + workInProgress.stateNode._visibility &= ~OffscreenDetached; + workInProgress.stateNode._visibility |= + workInProgress.stateNode._pendingVisibility & OffscreenDetached; + markRef(current, workInProgress); if ( diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js new file mode 100644 index 0000000000000..c86f1ab56bafa --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -0,0 +1,4218 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + ReactProviderType, + ReactContext, + ReactNodeList, + MutableSource, +} from 'shared/ReactTypes'; +import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; +import type {Fiber, FiberRoot} from './ReactInternalTypes'; +import type {TypeOfMode} from './ReactTypeOfMode'; +import type {Lanes, Lane} from './ReactFiberLane.old'; +import type { + SuspenseState, + SuspenseListRenderState, + SuspenseListTailMode, +} from './ReactFiberSuspenseComponent.old'; +import type {SuspenseContext} from './ReactFiberSuspenseContext.old'; +import type { + OffscreenProps, + OffscreenState, + OffscreenQueue, + OffscreenInstance, +} from './ReactFiberOffscreenComponent'; +import {OffscreenDetached} from './ReactFiberOffscreenComponent'; +import type { + Cache, + CacheComponentState, + SpawnedCachePool, +} from './ReactFiberCacheComponent.old'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; +import type {RootState} from './ReactFiberRoot.old'; +import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old'; + +import checkPropTypes from 'shared/checkPropTypes'; +import { + markComponentRenderStarted, + markComponentRenderStopped, + setIsStrictModeForDevtools, +} from './ReactFiberDevToolsHook.old'; +import { + IndeterminateComponent, + FunctionComponent, + ClassComponent, + HostRoot, + HostComponent, + HostResource, + HostSingleton, + HostText, + HostPortal, + ForwardRef, + Fragment, + Mode, + ContextProvider, + ContextConsumer, + Profiler, + SuspenseComponent, + SuspenseListComponent, + MemoComponent, + SimpleMemoComponent, + LazyComponent, + IncompleteClassComponent, + ScopeComponent, + OffscreenComponent, + LegacyHiddenComponent, + CacheComponent, + TracingMarkerComponent, +} from './ReactWorkTags'; +import { + NoFlags, + PerformedWork, + Placement, + Hydrating, + ContentReset, + DidCapture, + Update, + Ref, + RefStatic, + ChildDeletion, + ForceUpdateForLegacySuspense, + StaticMask, + ShouldCapture, + ForceClientRender, + Passive, +} from './ReactFiberFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import { + debugRenderPhaseSideEffectsForStrictMode, + disableLegacyContext, + disableModulePatternComponents, + enableProfilerCommitHooks, + enableProfilerTimer, + warnAboutDefaultPropsOnFunctionComponents, + enableScopeAPI, + enableCache, + enableLazyContextPropagation, + enableSchedulingProfiler, + enableTransitionTracing, + enableLegacyHidden, + enableCPUSuspense, + enableUseMutableSource, + enableFloat, + enableHostSingletons, +} from 'shared/ReactFeatureFlags'; +import isArray from 'shared/isArray'; +import shallowEqual from 'shared/shallowEqual'; +import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import getComponentNameFromType from 'shared/getComponentNameFromType'; +import ReactStrictModeWarnings from './ReactStrictModeWarnings.old'; +import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols'; +import { + getCurrentFiberOwnerNameInDevOrNull, + setIsRendering, +} from './ReactCurrentFiber'; +import { + resolveFunctionForHotReloading, + resolveForwardRefForHotReloading, + resolveClassForHotReloading, +} from './ReactFiberHotReloading.old'; + +import { + mountChildFibers, + reconcileChildFibers, + cloneChildFibers, +} from './ReactChildFiber.old'; +import { + processUpdateQueue, + cloneUpdateQueue, + initializeUpdateQueue, + enqueueCapturedUpdate, +} from './ReactFiberClassUpdateQueue.old'; +import { + NoLane, + NoLanes, + SyncLane, + OffscreenLane, + DefaultHydrationLane, + SomeRetryLane, + NoTimestamp, + includesSomeLane, + laneToLanes, + removeLanes, + mergeLanes, + getBumpedLaneForHydration, + pickArbitraryLane, +} from './ReactFiberLane.old'; +import { + ConcurrentMode, + NoMode, + ProfileMode, + StrictLegacyMode, +} from './ReactTypeOfMode'; +import { + shouldSetTextContent, + isSuspenseInstancePending, + isSuspenseInstanceFallback, + getSuspenseInstanceFallbackErrorDetails, + registerSuspenseInstanceRetry, + supportsHydration, + supportsResources, + supportsSingletons, + isPrimaryRenderer, + getResource, +} from './ReactFiberHostConfig'; +import type {SuspenseInstance} from './ReactFiberHostConfig'; +import {shouldError, shouldSuspend} from './ReactFiberReconciler'; +import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old'; +import { + suspenseStackCursor, + pushSuspenseListContext, + ForceSuspenseFallback, + hasSuspenseListContext, + setDefaultShallowSuspenseListContext, + setShallowSuspenseListContext, + pushPrimaryTreeSuspenseHandler, + pushFallbackTreeSuspenseHandler, + pushOffscreenSuspenseHandler, + reuseSuspenseHandlerOnStack, + popSuspenseHandler, +} from './ReactFiberSuspenseContext.old'; +import { + pushHiddenContext, + reuseHiddenContextOnStack, +} from './ReactFiberHiddenContext.old'; +import {findFirstSuspended} from './ReactFiberSuspenseComponent.old'; +import { + pushProvider, + propagateContextChange, + lazilyPropagateParentContextChanges, + propagateParentContextChangesToDeferredTree, + checkIfContextChanged, + readContext, + prepareToReadContext, + scheduleContextWorkOnParentPath, +} from './ReactFiberNewContext.old'; +import { + renderWithHooks, + checkDidRenderIdHook, + bailoutHooks, + replaySuspendedComponentWithHooks, +} from './ReactFiberHooks.old'; +import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old'; +import { + getMaskedContext, + getUnmaskedContext, + hasContextChanged as hasLegacyContextChanged, + pushContextProvider as pushLegacyContextProvider, + isContextProvider as isLegacyContextProvider, + pushTopLevelContextObject, + invalidateContextProvider, +} from './ReactFiberContext.old'; +import { + getIsHydrating, + enterHydrationState, + reenterHydrationStateFromDehydratedSuspenseInstance, + resetHydrationState, + claimHydratableSingleton, + tryToClaimNextHydratableInstance, + warnIfHydrating, + queueHydrationError, +} from './ReactFiberHydrationContext.old'; +import { + adoptClassInstance, + constructClassInstance, + mountClassInstance, + resumeMountClassInstance, + updateClassInstance, +} from './ReactFiberClassComponent.old'; +import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; +import { + resolveLazyComponentTag, + createFiberFromTypeAndProps, + createFiberFromFragment, + createFiberFromOffscreen, + createWorkInProgress, + isSimpleFunctionComponent, +} from './ReactFiber.old'; +import { + retryDehydratedSuspenseBoundary, + scheduleUpdateOnFiber, + renderDidSuspendDelayIfPossible, + markSkippedUpdateLanes, + getWorkInProgressRoot, +} from './ReactFiberWorkLoop.old'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; +import {setWorkInProgressVersion} from './ReactMutableSource.old'; +import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.old'; +import { + createCapturedValue, + createCapturedValueAtFiber, + type CapturedValue, +} from './ReactCapturedValue'; +import {createClassErrorUpdate} from './ReactFiberThrow.old'; +import is from 'shared/objectIs'; +import { + getForksAtLevel, + isForkedChild, + pushTreeId, + pushMaterializedTreeId, +} from './ReactFiberTreeContext.old'; +import { + requestCacheFromPool, + pushRootTransition, + getSuspendedCache, + pushTransition, + getOffscreenDeferredCache, + getPendingTransitions, +} from './ReactFiberTransition.old'; +import { + getMarkerInstances, + pushMarkerInstance, + pushRootMarkerInstance, + TransitionTracingMarker, +} from './ReactFiberTracingMarkerComponent.old'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + +// A special exception that's used to unwind the stack when an update flows +// into a dehydrated boundary. +export const SelectiveHydrationException: mixed = new Error( + "This is not a real error. It's an implementation detail of React's " + + "selective hydration feature. If this leaks into userspace, it's a bug in " + + 'React. Please file an issue.', +); + +let didReceiveUpdate: boolean = false; + +let didWarnAboutBadClass; +let didWarnAboutModulePatternComponent; +let didWarnAboutContextTypeOnFunctionComponent; +let didWarnAboutGetDerivedStateOnFunctionComponent; +let didWarnAboutFunctionRefs; +export let didWarnAboutReassigningProps: boolean; +let didWarnAboutRevealOrder; +let didWarnAboutTailOptions; +let didWarnAboutDefaultPropsOnFunctionComponent; + +if (__DEV__) { + didWarnAboutBadClass = {}; + didWarnAboutModulePatternComponent = {}; + didWarnAboutContextTypeOnFunctionComponent = {}; + didWarnAboutGetDerivedStateOnFunctionComponent = {}; + didWarnAboutFunctionRefs = {}; + didWarnAboutReassigningProps = false; + didWarnAboutRevealOrder = {}; + didWarnAboutTailOptions = {}; + didWarnAboutDefaultPropsOnFunctionComponent = {}; +} + +export function reconcileChildren( + current: Fiber | null, + workInProgress: Fiber, + nextChildren: any, + renderLanes: Lanes, +) { + if (current === null) { + // If this is a fresh new component that hasn't been rendered yet, we + // won't update its child set by applying minimal side-effects. Instead, + // we will add them all to the child before it gets rendered. That means + // we can optimize this reconciliation pass by not tracking side-effects. + workInProgress.child = mountChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); + } else { + // If the current child is the same as the work in progress, it means that + // we haven't yet started any work on these children. Therefore, we use + // the clone algorithm to create a copy of all the current children. + + // If we had any progressed work already, that is invalid at this point so + // let's throw it out. + workInProgress.child = reconcileChildFibers( + workInProgress, + current.child, + nextChildren, + renderLanes, + ); + } +} + +function forceUnmountCurrentAndReconcile( + current: Fiber, + workInProgress: Fiber, + nextChildren: any, + renderLanes: Lanes, +) { + // This function is fork of reconcileChildren. It's used in cases where we + // want to reconcile without matching against the existing set. This has the + // effect of all current children being unmounted; even if the type and key + // are the same, the old child is unmounted and a new child is created. + // + // To do this, we're going to go through the reconcile algorithm twice. In + // the first pass, we schedule a deletion for all the current children by + // passing null. + workInProgress.child = reconcileChildFibers( + workInProgress, + current.child, + null, + renderLanes, + ); + // In the second pass, we mount the new children. The trick here is that we + // pass null in place of where we usually pass the current child set. This has + // the effect of remounting all children regardless of whether their + // identities match. + workInProgress.child = reconcileChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); +} + +function updateForwardRef( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + renderLanes: Lanes, +) { + // TODO: current can be non-null here even if the component + // hasn't yet mounted. This happens after the first render suspends. + // We'll need to figure out if this is fine or can cause issues. + + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + const innerPropTypes = Component.propTypes; + if (innerPropTypes) { + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentNameFromType(Component), + ); + } + } + } + + const render = Component.render; + const ref = workInProgress.ref; + + // The rest is a fork of updateFunctionComponent + let nextChildren; + let hasId; + prepareToReadContext(workInProgress, renderLanes); + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setIsRendering(true); + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + ref, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + setIsRendering(false); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + ref, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + } + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderLanes); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateMemoComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + renderLanes: Lanes, +): null | Fiber { + if (current === null) { + const type = Component.type; + if ( + isSimpleFunctionComponent(type) && + Component.compare === null && + // SimpleMemoComponent codepath doesn't resolve outer props either. + Component.defaultProps === undefined + ) { + let resolvedType = type; + if (__DEV__) { + resolvedType = resolveFunctionForHotReloading(type); + } + // If this is a plain function component without default props, + // and with only the default shallow comparison, we upgrade it + // to a SimpleMemoComponent to allow fast path updates. + workInProgress.tag = SimpleMemoComponent; + workInProgress.type = resolvedType; + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, type); + } + return updateSimpleMemoComponent( + current, + workInProgress, + resolvedType, + nextProps, + renderLanes, + ); + } + if (__DEV__) { + const innerPropTypes = type.propTypes; + if (innerPropTypes) { + // Inner memo component props aren't currently validated in createElement. + // We could move it there, but we'd still need this for lazy code path. + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentNameFromType(type), + ); + } + if ( + warnAboutDefaultPropsOnFunctionComponents && + Component.defaultProps !== undefined + ) { + const componentName = getComponentNameFromType(type) || 'Unknown'; + if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { + console.error( + '%s: Support for defaultProps will be removed from memo components ' + + 'in a future major release. Use JavaScript default parameters instead.', + componentName, + ); + didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; + } + } + } + const child = createFiberFromTypeAndProps( + Component.type, + null, + nextProps, + workInProgress, + workInProgress.mode, + renderLanes, + ); + child.ref = workInProgress.ref; + child.return = workInProgress; + workInProgress.child = child; + return child; + } + if (__DEV__) { + const type = Component.type; + const innerPropTypes = type.propTypes; + if (innerPropTypes) { + // Inner memo component props aren't currently validated in createElement. + // We could move it there, but we'd still need this for lazy code path. + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentNameFromType(type), + ); + } + } + const currentChild = ((current.child: any): Fiber); // This is always exactly one child + const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( + current, + renderLanes, + ); + if (!hasScheduledUpdateOrContext) { + // This will be the props with resolved defaultProps, + // unlike current.memoizedProps which will be the unresolved ones. + const prevProps = currentChild.memoizedProps; + // Default to shallow comparison + let compare = Component.compare; + compare = compare !== null ? compare : shallowEqual; + if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + } + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + const newChild = createWorkInProgress(currentChild, nextProps); + newChild.ref = workInProgress.ref; + newChild.return = workInProgress; + workInProgress.child = newChild; + return newChild; +} + +function updateSimpleMemoComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + renderLanes: Lanes, +): null | Fiber { + // TODO: current can be non-null here even if the component + // hasn't yet mounted. This happens when the inner render suspends. + // We'll need to figure out if this is fine or can cause issues. + + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + let outerMemoType = workInProgress.elementType; + if (outerMemoType.$$typeof === REACT_LAZY_TYPE) { + // We warn when you define propTypes on lazy() + // so let's just skip over it to find memo() outer wrapper. + // Inner props for memo are validated later. + const lazyComponent: LazyComponentType = outerMemoType; + const payload = lazyComponent._payload; + const init = lazyComponent._init; + try { + outerMemoType = init(payload); + } catch (x) { + // $FlowFixMe[incompatible-type] found when upgrading Flow + outerMemoType = null; + } + // Inner propTypes will be validated in the function component path. + const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes; + if (outerPropTypes) { + checkPropTypes( + outerPropTypes, + nextProps, // Resolved (SimpleMemoComponent has no defaultProps) + 'prop', + getComponentNameFromType(outerMemoType), + ); + } + } + } + } + if (current !== null) { + const prevProps = current.memoizedProps; + if ( + shallowEqual(prevProps, nextProps) && + current.ref === workInProgress.ref && + // Prevent bailout if the implementation changed due to hot reload. + (__DEV__ ? workInProgress.type === current.type : true) + ) { + didReceiveUpdate = false; + + // The props are shallowly equal. Reuse the previous props object, like we + // would during a normal fiber bailout. + // + // We don't have strong guarantees that the props object is referentially + // equal during updates where we can't bail out anyway — like if the props + // are shallowly equal, but there's a local state or context update in the + // same batch. + // + // However, as a principle, we should aim to make the behavior consistent + // across different ways of memoizing a component. For example, React.memo + // has a different internal Fiber layout if you pass a normal function + // component (SimpleMemoComponent) versus if you pass a different type + // like forwardRef (MemoComponent). But this is an implementation detail. + // Wrapping a component in forwardRef (or React.lazy, etc) shouldn't + // affect whether the props object is reused during a bailout. + workInProgress.pendingProps = nextProps = prevProps; + + if (!checkScheduledUpdateOrContext(current, renderLanes)) { + // The pending lanes were cleared at the beginning of beginWork. We're + // about to bail out, but there might be other lanes that weren't + // included in the current render. Usually, the priority level of the + // remaining updates is accumulated during the evaluation of the + // component (i.e. when processing the update queue). But since since + // we're bailing out early *without* evaluating the component, we need + // to account for it here, too. Reset to the value of the current fiber. + // NOTE: This only applies to SimpleMemoComponent, not MemoComponent, + // because a MemoComponent fiber does not have hooks or an update queue; + // rather, it wraps around an inner component, which may or may not + // contains hooks. + // TODO: Move the reset at in beginWork out of the common path so that + // this is no longer necessary. + workInProgress.lanes = current.lanes; + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderLanes, + ); + } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { + // This is a special case that only exists for legacy mode. + // See https://github.com/facebook/react/pull/19216. + didReceiveUpdate = true; + } + } + } + return updateFunctionComponent( + current, + workInProgress, + Component, + nextProps, + renderLanes, + ); +} + +function updateOffscreenComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + const nextProps: OffscreenProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + + const prevState: OffscreenState | null = + current !== null ? current.memoizedState : null; + + // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is + // to support batching of `attach` and `detach` calls. + workInProgress.stateNode._visibility &= ~OffscreenDetached; + workInProgress.stateNode._visibility |= + workInProgress.stateNode._pendingVisibility & OffscreenDetached; + + markRef(current, workInProgress); + + if ( + nextProps.mode === 'hidden' || + (enableLegacyHidden && + nextProps.mode === 'unstable-defer-without-hiding') || + // TODO: remove read from stateNode. + workInProgress.stateNode._visibility & OffscreenDetached + ) { + // Rendering a hidden tree. + + const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags; + if (didSuspend) { + // Something suspended inside a hidden tree + + // Include the base lanes from the last render + const nextBaseLanes = + prevState !== null + ? mergeLanes(prevState.baseLanes, renderLanes) + : renderLanes; + + if (current !== null) { + // Reset to the current children + let currentChild = (workInProgress.child = current.child); + + // The current render suspended, but there may be other lanes with + // pending work. We can't read `childLanes` from the current Offscreen + // fiber because we reset it when it was deferred; however, we can read + // the pending lanes from the child fibers. + let currentChildLanes = NoLanes; + while (currentChild !== null) { + currentChildLanes = mergeLanes( + mergeLanes(currentChildLanes, currentChild.lanes), + currentChild.childLanes, + ); + currentChild = currentChild.sibling; + } + const lanesWeJustAttempted = nextBaseLanes; + const remainingChildLanes = removeLanes( + currentChildLanes, + lanesWeJustAttempted, + ); + workInProgress.childLanes = remainingChildLanes; + } else { + workInProgress.childLanes = NoLanes; + workInProgress.child = null; + } + + return deferHiddenOffscreenComponent( + current, + workInProgress, + nextBaseLanes, + renderLanes, + ); + } + + if ((workInProgress.mode & ConcurrentMode) === NoMode) { + // In legacy sync mode, don't defer the subtree. Render it now. + // TODO: Consider how Offscreen should work with transitions in the future + const nextState: OffscreenState = { + baseLanes: NoLanes, + cachePool: null, + }; + workInProgress.memoizedState = nextState; + if (enableCache) { + // push the cache pool even though we're going to bail out + // because otherwise there'd be a context mismatch + if (current !== null) { + pushTransition(workInProgress, null, null); + } + } + reuseHiddenContextOnStack(workInProgress); + pushOffscreenSuspenseHandler(workInProgress); + } else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) { + // We're hidden, and we're not rendering at Offscreen. We will bail out + // and resume this tree later. + + // Schedule this fiber to re-render at Offscreen priority + workInProgress.lanes = workInProgress.childLanes = laneToLanes( + OffscreenLane, + ); + + // Include the base lanes from the last render + const nextBaseLanes = + prevState !== null + ? mergeLanes(prevState.baseLanes, renderLanes) + : renderLanes; + + return deferHiddenOffscreenComponent( + current, + workInProgress, + nextBaseLanes, + renderLanes, + ); + } else { + // This is the second render. The surrounding visible content has already + // committed. Now we resume rendering the hidden tree. + + // Rendering at offscreen, so we can clear the base lanes. + const nextState: OffscreenState = { + baseLanes: NoLanes, + cachePool: null, + }; + workInProgress.memoizedState = nextState; + if (enableCache && current !== null) { + // If the render that spawned this one accessed the cache pool, resume + // using the same cache. Unless the parent changed, since that means + // there was a refresh. + const prevCachePool = prevState !== null ? prevState.cachePool : null; + // TODO: Consider if and how Offscreen pre-rendering should + // be attributed to the transition that spawned it + pushTransition(workInProgress, prevCachePool, null); + } + + // Push the lanes that were skipped when we bailed out. + if (prevState !== null) { + pushHiddenContext(workInProgress, prevState); + } else { + reuseHiddenContextOnStack(workInProgress); + } + pushOffscreenSuspenseHandler(workInProgress); + } + } else { + // Rendering a visible tree. + if (prevState !== null) { + // We're going from hidden -> visible. + let prevCachePool = null; + if (enableCache) { + // If the render that spawned this one accessed the cache pool, resume + // using the same cache. Unless the parent changed, since that means + // there was a refresh. + prevCachePool = prevState.cachePool; + } + + let transitions = null; + if (enableTransitionTracing) { + // We have now gone from hidden to visible, so any transitions should + // be added to the stack to get added to any Offscreen/suspense children + const instance: OffscreenInstance | null = workInProgress.stateNode; + if (instance !== null && instance._transitions != null) { + transitions = Array.from(instance._transitions); + } + } + + pushTransition(workInProgress, prevCachePool, transitions); + + // Push the lanes that were skipped when we bailed out. + pushHiddenContext(workInProgress, prevState); + reuseSuspenseHandlerOnStack(workInProgress); + + // Since we're not hidden anymore, reset the state + workInProgress.memoizedState = null; + } else { + // We weren't previously hidden, and we still aren't, so there's nothing + // special to do. Need to push to the stack regardless, though, to avoid + // a push/pop misalignment. + + if (enableCache) { + // If the render that spawned this one accessed the cache pool, resume + // using the same cache. Unless the parent changed, since that means + // there was a refresh. + if (current !== null) { + pushTransition(workInProgress, null, null); + } + } + + // We're about to bail out, but we need to push this to the stack anyway + // to avoid a push/pop misalignment. + reuseHiddenContextOnStack(workInProgress); + reuseSuspenseHandlerOnStack(workInProgress); + } + } + + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function deferHiddenOffscreenComponent( + current: Fiber | null, + workInProgress: Fiber, + nextBaseLanes: Lanes, + renderLanes: Lanes, +) { + const nextState: OffscreenState = { + baseLanes: nextBaseLanes, + // Save the cache pool so we can resume later. + cachePool: enableCache ? getOffscreenDeferredCache() : null, + }; + workInProgress.memoizedState = nextState; + if (enableCache) { + // push the cache pool even though we're going to bail out + // because otherwise there'd be a context mismatch + if (current !== null) { + pushTransition(workInProgress, null, null); + } + } + + // We're about to bail out, but we need to push this to the stack anyway + // to avoid a push/pop misalignment. + reuseHiddenContextOnStack(workInProgress); + + pushOffscreenSuspenseHandler(workInProgress); + + if (enableLazyContextPropagation && current !== null) { + // Since this tree will resume rendering in a separate render, we need + // to propagate parent contexts now so we don't lose track of which + // ones changed. + propagateParentContextChangesToDeferredTree( + current, + workInProgress, + renderLanes, + ); + } + + return null; +} + +// Note: These happen to have identical begin phases, for now. We shouldn't hold +// ourselves to this constraint, though. If the behavior diverges, we should +// fork the function. +const updateLegacyHiddenComponent = updateOffscreenComponent; + +function updateCacheComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + if (!enableCache) { + return null; + } + + prepareToReadContext(workInProgress, renderLanes); + const parentCache = readContext(CacheContext); + + if (current === null) { + // Initial mount. Request a fresh cache from the pool. + const freshCache = requestCacheFromPool(renderLanes); + const initialState: CacheComponentState = { + parent: parentCache, + cache: freshCache, + }; + workInProgress.memoizedState = initialState; + initializeUpdateQueue(workInProgress); + pushCacheProvider(workInProgress, freshCache); + } else { + // Check for updates + if (includesSomeLane(current.lanes, renderLanes)) { + cloneUpdateQueue(current, workInProgress); + processUpdateQueue(workInProgress, null, null, renderLanes); + } + const prevState: CacheComponentState = current.memoizedState; + const nextState: CacheComponentState = workInProgress.memoizedState; + + // Compare the new parent cache to the previous to see detect there was + // a refresh. + if (prevState.parent !== parentCache) { + // Refresh in parent. Update the parent. + const derivedState: CacheComponentState = { + parent: parentCache, + cache: parentCache, + }; + + // Copied from getDerivedStateFromProps implementation. Once the update + // queue is empty, persist the derived state onto the base state. + workInProgress.memoizedState = derivedState; + if (workInProgress.lanes === NoLanes) { + const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + workInProgress.memoizedState = updateQueue.baseState = derivedState; + } + + pushCacheProvider(workInProgress, parentCache); + // No need to propagate a context change because the refreshed parent + // already did. + } else { + // The parent didn't refresh. Now check if this cache did. + const nextCache = nextState.cache; + pushCacheProvider(workInProgress, nextCache); + if (nextCache !== prevState.cache) { + // This cache refreshed. Propagate a context change. + propagateContextChange(workInProgress, CacheContext, renderLanes); + } + } + } + + const nextChildren = workInProgress.pendingProps.children; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +// This should only be called if the name changes +function updateTracingMarkerComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + if (!enableTransitionTracing) { + return null; + } + + // TODO: (luna) Only update the tracing marker if it's newly rendered or it's name changed. + // A tracing marker is only associated with the transitions that rendered + // or updated it, so we can create a new set of transitions each time + if (current === null) { + const currentTransitions = getPendingTransitions(); + if (currentTransitions !== null) { + const markerInstance: TracingMarkerInstance = { + tag: TransitionTracingMarker, + transitions: new Set(currentTransitions), + pendingBoundaries: null, + name: workInProgress.pendingProps.name, + aborts: null, + }; + workInProgress.stateNode = markerInstance; + + // We call the marker complete callback when all child suspense boundaries resolve. + // We do this in the commit phase on Offscreen. If the marker has no child suspense + // boundaries, we need to schedule a passive effect to make sure we call the marker + // complete callback. + workInProgress.flags |= Passive; + } + } else { + if (__DEV__) { + if (current.memoizedProps.name !== workInProgress.pendingProps.name) { + console.error( + 'Changing the name of a tracing marker after mount is not supported. ' + + 'To remount the tracing marker, pass it a new key.', + ); + } + } + } + + const instance: TracingMarkerInstance | null = workInProgress.stateNode; + if (instance !== null) { + pushMarkerInstance(workInProgress, instance); + } + const nextChildren = workInProgress.pendingProps.children; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateFragment( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + const nextChildren = workInProgress.pendingProps; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateMode( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + const nextChildren = workInProgress.pendingProps.children; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateProfiler( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + if (enableProfilerTimer) { + workInProgress.flags |= Update; + + if (enableProfilerCommitHooks) { + // Reset effect durations for the next eventual effect phase. + // These are reset during render to allow the DevTools commit hook a chance to read them, + const stateNode = workInProgress.stateNode; + stateNode.effectDuration = 0; + stateNode.passiveEffectDuration = 0; + } + } + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function markRef(current: Fiber | null, workInProgress: Fiber) { + const ref = workInProgress.ref; + if ( + (current === null && ref !== null) || + (current !== null && current.ref !== ref) + ) { + // Schedule a Ref effect + workInProgress.flags |= Ref; + workInProgress.flags |= RefStatic; + } +} + +function updateFunctionComponent( + current, + workInProgress, + Component, + nextProps: any, + renderLanes, +) { + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + const innerPropTypes = Component.propTypes; + if (innerPropTypes) { + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentNameFromType(Component), + ); + } + } + } + + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); + context = getMaskedContext(workInProgress, unmaskedContext); + } + + let nextChildren; + let hasId; + prepareToReadContext(workInProgress, renderLanes); + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setIsRendering(true); + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + setIsRendering(false); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + } + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderLanes); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +export function replayFunctionComponent( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + Component: any, + renderLanes: Lanes, +): Fiber | null { + // This function is used to replay a component that previously suspended, + // after its data resolves. It's a simplified version of + // updateFunctionComponent that reuses the hooks from the previous attempt. + + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); + context = getMaskedContext(workInProgress, unmaskedContext); + } + + prepareToReadContext(workInProgress, renderLanes); + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + const nextChildren = replaySuspendedComponentWithHooks( + current, + workInProgress, + Component, + nextProps, + context, + ); + const hasId = checkDidRenderIdHook(); + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderLanes); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateClassComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + nextProps: any, + renderLanes: Lanes, +) { + if (__DEV__) { + // This is used by DevTools to force a boundary to error. + switch (shouldError(workInProgress)) { + case false: { + const instance = workInProgress.stateNode; + const ctor = workInProgress.type; + // TODO This way of resetting the error boundary state is a hack. + // Is there a better way to do this? + const tempInstance = new ctor( + workInProgress.memoizedProps, + instance.context, + ); + const state = tempInstance.state; + instance.updater.enqueueSetState(instance, state, null); + break; + } + case true: { + workInProgress.flags |= DidCapture; + workInProgress.flags |= ShouldCapture; + // eslint-disable-next-line react-internal/prod-error-codes + const error = new Error('Simulated error coming from DevTools'); + const lane = pickArbitraryLane(renderLanes); + workInProgress.lanes = mergeLanes(workInProgress.lanes, lane); + // Schedule the error boundary to re-render using updated state + const update = createClassErrorUpdate( + workInProgress, + createCapturedValueAtFiber(error, workInProgress), + lane, + ); + enqueueCapturedUpdate(workInProgress, update); + break; + } + } + + if (workInProgress.type !== workInProgress.elementType) { + // Lazy component props can't be validated in createElement + // because they're only guaranteed to be resolved here. + const innerPropTypes = Component.propTypes; + if (innerPropTypes) { + checkPropTypes( + innerPropTypes, + nextProps, // Resolved props + 'prop', + getComponentNameFromType(Component), + ); + } + } + } + + // Push context providers early to prevent context stack mismatches. + // During mounting we don't know the child context yet as the instance doesn't exist. + // We will invalidate the child context in finishClassComponent() right after rendering. + let hasContext; + if (isLegacyContextProvider(Component)) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } + prepareToReadContext(workInProgress, renderLanes); + + const instance = workInProgress.stateNode; + let shouldUpdate; + if (instance === null) { + resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress); + + // In the initial pass we might need to construct the instance. + constructClassInstance(workInProgress, Component, nextProps); + mountClassInstance(workInProgress, Component, nextProps, renderLanes); + shouldUpdate = true; + } else if (current === null) { + // In a resume, we'll already have an instance we can reuse. + shouldUpdate = resumeMountClassInstance( + workInProgress, + Component, + nextProps, + renderLanes, + ); + } else { + shouldUpdate = updateClassInstance( + current, + workInProgress, + Component, + nextProps, + renderLanes, + ); + } + const nextUnitOfWork = finishClassComponent( + current, + workInProgress, + Component, + shouldUpdate, + hasContext, + renderLanes, + ); + if (__DEV__) { + const inst = workInProgress.stateNode; + if (shouldUpdate && inst.props !== nextProps) { + if (!didWarnAboutReassigningProps) { + console.error( + 'It looks like %s is reassigning its own `this.props` while rendering. ' + + 'This is not supported and can lead to confusing bugs.', + getComponentNameFromFiber(workInProgress) || 'a component', + ); + } + didWarnAboutReassigningProps = true; + } + } + return nextUnitOfWork; +} + +function finishClassComponent( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + shouldUpdate: boolean, + hasContext: boolean, + renderLanes: Lanes, +) { + // Refs should update even if shouldComponentUpdate returns false + markRef(current, workInProgress); + + const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags; + + if (!shouldUpdate && !didCaptureError) { + // Context providers should defer to sCU for rendering + if (hasContext) { + invalidateContextProvider(workInProgress, Component, false); + } + + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + + const instance = workInProgress.stateNode; + + // Rerender + ReactCurrentOwner.current = workInProgress; + let nextChildren; + if ( + didCaptureError && + typeof Component.getDerivedStateFromError !== 'function' + ) { + // If we captured an error, but getDerivedStateFromError is not defined, + // unmount all the children. componentDidCatch will schedule an update to + // re-render a fallback. This is temporary until we migrate everyone to + // the new API. + // TODO: Warn in a future release. + nextChildren = null; + + if (enableProfilerTimer) { + stopProfilerTimerIfRunning(workInProgress); + } + } else { + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + if (__DEV__) { + setIsRendering(true); + nextChildren = instance.render(); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictLegacyMode + ) { + setIsStrictModeForDevtools(true); + try { + instance.render(); + } finally { + setIsStrictModeForDevtools(false); + } + } + setIsRendering(false); + } else { + nextChildren = instance.render(); + } + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + if (current !== null && didCaptureError) { + // If we're recovering from an error, reconcile without reusing any of + // the existing children. Conceptually, the normal children and the children + // that are shown on error are two different sets, so we shouldn't reuse + // normal children even if their identities match. + forceUnmountCurrentAndReconcile( + current, + workInProgress, + nextChildren, + renderLanes, + ); + } else { + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + } + + // Memoize state using the values we just used to render. + // TODO: Restructure so we never read values from the instance. + workInProgress.memoizedState = instance.state; + + // The context might have changed so we need to recalculate it. + if (hasContext) { + invalidateContextProvider(workInProgress, Component, true); + } + + return workInProgress.child; +} + +function pushHostRootContext(workInProgress) { + const root = (workInProgress.stateNode: FiberRoot); + if (root.pendingContext) { + pushTopLevelContextObject( + workInProgress, + root.pendingContext, + root.pendingContext !== root.context, + ); + } else if (root.context) { + // Should always be set + pushTopLevelContextObject(workInProgress, root.context, false); + } + pushHostContainer(workInProgress, root.containerInfo); +} + +function updateHostRoot(current, workInProgress, renderLanes) { + pushHostRootContext(workInProgress); + + if (current === null) { + throw new Error('Should have a current fiber. This is a bug in React.'); + } + + const nextProps = workInProgress.pendingProps; + const prevState = workInProgress.memoizedState; + const prevChildren = prevState.element; + cloneUpdateQueue(current, workInProgress); + processUpdateQueue(workInProgress, nextProps, null, renderLanes); + + const nextState: RootState = workInProgress.memoizedState; + const root: FiberRoot = workInProgress.stateNode; + pushRootTransition(workInProgress, root, renderLanes); + + if (enableTransitionTracing) { + pushRootMarkerInstance(workInProgress); + } + + if (enableCache) { + const nextCache: Cache = nextState.cache; + pushCacheProvider(workInProgress, nextCache); + if (nextCache !== prevState.cache) { + // The root cache refreshed. + propagateContextChange(workInProgress, CacheContext, renderLanes); + } + } + + // Caution: React DevTools currently depends on this property + // being called "element". + const nextChildren = nextState.element; + if (supportsHydration && prevState.isDehydrated) { + // This is a hydration root whose shell has not yet hydrated. We should + // attempt to hydrate. + + // Flip isDehydrated to false to indicate that when this render + // finishes, the root will no longer be dehydrated. + const overrideState: RootState = { + element: nextChildren, + isDehydrated: false, + cache: nextState.cache, + }; + const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + // `baseState` can always be the last state because the root doesn't + // have reducer functions so it doesn't need rebasing. + updateQueue.baseState = overrideState; + workInProgress.memoizedState = overrideState; + + if (workInProgress.flags & ForceClientRender) { + // Something errored during a previous attempt to hydrate the shell, so we + // forced a client render. + const recoverableError = createCapturedValueAtFiber( + new Error( + 'There was an error while hydrating. Because the error happened outside ' + + 'of a Suspense boundary, the entire root will switch to ' + + 'client rendering.', + ), + workInProgress, + ); + return mountHostRootWithoutHydrating( + current, + workInProgress, + nextChildren, + renderLanes, + recoverableError, + ); + } else if (nextChildren !== prevChildren) { + const recoverableError = createCapturedValueAtFiber( + new Error( + 'This root received an early update, before anything was able ' + + 'hydrate. Switched the entire root to client rendering.', + ), + workInProgress, + ); + return mountHostRootWithoutHydrating( + current, + workInProgress, + nextChildren, + renderLanes, + recoverableError, + ); + } else { + // The outermost shell has not hydrated yet. Start hydrating. + enterHydrationState(workInProgress); + if (enableUseMutableSource) { + const mutableSourceEagerHydrationData = + root.mutableSourceEagerHydrationData; + if (mutableSourceEagerHydrationData != null) { + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { + const mutableSource = ((mutableSourceEagerHydrationData[ + i + ]: any): MutableSource); + const version = mutableSourceEagerHydrationData[i + 1]; + setWorkInProgressVersion(mutableSource, version); + } + } + } + + const child = mountChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); + workInProgress.child = child; + + let node = child; + while (node) { + // Mark each child as hydrating. This is a fast path to know whether this + // tree is part of a hydrating tree. This is used to determine if a child + // node has fully mounted yet, and for scheduling event replaying. + // Conceptually this is similar to Placement in that a new subtree is + // inserted into the React tree here. It just happens to not need DOM + // mutations because it already exists. + node.flags = (node.flags & ~Placement) | Hydrating; + node = node.sibling; + } + } + } else { + // Root is not dehydrated. Either this is a client-only root, or it + // already hydrated. + resetHydrationState(); + if (nextChildren === prevChildren) { + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + } + return workInProgress.child; +} + +function mountHostRootWithoutHydrating( + current: Fiber, + workInProgress: Fiber, + nextChildren: ReactNodeList, + renderLanes: Lanes, + recoverableError: CapturedValue, +) { + // Revert to client rendering. + resetHydrationState(); + + queueHydrationError(recoverableError); + + workInProgress.flags |= ForceClientRender; + + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateHostComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + pushHostContext(workInProgress); + + if (current === null) { + tryToClaimNextHydratableInstance(workInProgress); + } + + const type = workInProgress.type; + const nextProps = workInProgress.pendingProps; + const prevProps = current !== null ? current.memoizedProps : null; + + let nextChildren = nextProps.children; + const isDirectTextChild = shouldSetTextContent(type, nextProps); + + if (isDirectTextChild) { + // We special case a direct text child of a host node. This is a common + // case. We won't handle it as a reified child. We will instead handle + // this in the host environment that also has access to this prop. That + // avoids allocating another HostText fiber and traversing it. + nextChildren = null; + } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { + // If we're switching from a direct text child to a normal child, or to + // empty, we need to schedule the text content to be reset. + workInProgress.flags |= ContentReset; + } + + markRef(current, workInProgress); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +function updateHostResource(current, workInProgress, renderLanes) { + pushHostContext(workInProgress); + markRef(current, workInProgress); + const currentProps = current === null ? null : current.memoizedProps; + workInProgress.memoizedState = getResource( + workInProgress.type, + workInProgress.pendingProps, + currentProps, + ); + // Resources never have reconciler managed children. It is possible for + // the host implementation of getResource to consider children in the + // resource construction but they will otherwise be discarded. In practice + // this precludes all but the simplest children and Host specific warnings + // should be implemented to warn when children are passsed when otherwise not + // expected + return null; +} + +function updateHostSingleton( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + pushHostContext(workInProgress); + + if (current === null) { + claimHydratableSingleton(workInProgress); + } + + const nextChildren = workInProgress.pendingProps.children; + + if (current === null && !getIsHydrating()) { + // Similar to Portals we append Singleton children in the commit phase. So we + // Track insertions even on mount. + // TODO: Consider unifying this with how the root works. + workInProgress.child = reconcileChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); + } else { + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + } + markRef(current, workInProgress); + return workInProgress.child; +} + +function updateHostText(current, workInProgress) { + if (current === null) { + tryToClaimNextHydratableInstance(workInProgress); + } + // Nothing to do here. This is terminal. We'll do the completion step + // immediately after. + return null; +} + +function mountLazyComponent( + _current, + workInProgress, + elementType, + renderLanes, +) { + resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); + + const props = workInProgress.pendingProps; + const lazyComponent: LazyComponentType = elementType; + const payload = lazyComponent._payload; + const init = lazyComponent._init; + let Component = init(payload); + // Store the unwrapped component in the type. + workInProgress.type = Component; + const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component)); + const resolvedProps = resolveDefaultProps(Component, props); + let child; + switch (resolvedTag) { + case FunctionComponent: { + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, Component); + workInProgress.type = Component = resolveFunctionForHotReloading( + Component, + ); + } + child = updateFunctionComponent( + null, + workInProgress, + Component, + resolvedProps, + renderLanes, + ); + return child; + } + case ClassComponent: { + if (__DEV__) { + workInProgress.type = Component = resolveClassForHotReloading( + Component, + ); + } + child = updateClassComponent( + null, + workInProgress, + Component, + resolvedProps, + renderLanes, + ); + return child; + } + case ForwardRef: { + if (__DEV__) { + workInProgress.type = Component = resolveForwardRefForHotReloading( + Component, + ); + } + child = updateForwardRef( + null, + workInProgress, + Component, + resolvedProps, + renderLanes, + ); + return child; + } + case MemoComponent: { + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + const outerPropTypes = Component.propTypes; + if (outerPropTypes) { + checkPropTypes( + outerPropTypes, + resolvedProps, // Resolved for outer only + 'prop', + getComponentNameFromType(Component), + ); + } + } + } + child = updateMemoComponent( + null, + workInProgress, + Component, + resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too + renderLanes, + ); + return child; + } + } + let hint = ''; + if (__DEV__) { + if ( + Component !== null && + typeof Component === 'object' && + Component.$$typeof === REACT_LAZY_TYPE + ) { + hint = ' Did you wrap a component in React.lazy() more than once?'; + } + } + + // This message intentionally doesn't mention ForwardRef or MemoComponent + // because the fact that it's a separate type of work is an + // implementation detail. + throw new Error( + `Element type is invalid. Received a promise that resolves to: ${Component}. ` + + `Lazy element type must resolve to a class or function.${hint}`, + ); +} + +function mountIncompleteClassComponent( + _current, + workInProgress, + Component, + nextProps, + renderLanes, +) { + resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); + + // Promote the fiber to a class and try rendering again. + workInProgress.tag = ClassComponent; + + // The rest of this function is a fork of `updateClassComponent` + + // Push context providers early to prevent context stack mismatches. + // During mounting we don't know the child context yet as the instance doesn't exist. + // We will invalidate the child context in finishClassComponent() right after rendering. + let hasContext; + if (isLegacyContextProvider(Component)) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } + prepareToReadContext(workInProgress, renderLanes); + + constructClassInstance(workInProgress, Component, nextProps); + mountClassInstance(workInProgress, Component, nextProps, renderLanes); + + return finishClassComponent( + null, + workInProgress, + Component, + true, + hasContext, + renderLanes, + ); +} + +function mountIndeterminateComponent( + _current, + workInProgress, + Component, + renderLanes, +) { + resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); + + const props = workInProgress.pendingProps; + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext( + workInProgress, + Component, + false, + ); + context = getMaskedContext(workInProgress, unmaskedContext); + } + + prepareToReadContext(workInProgress, renderLanes); + let value; + let hasId; + + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + if (__DEV__) { + if ( + Component.prototype && + typeof Component.prototype.render === 'function' + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + + if (!didWarnAboutBadClass[componentName]) { + console.error( + "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + + 'This is likely to cause errors. Change %s to extend React.Component instead.', + componentName, + componentName, + ); + didWarnAboutBadClass[componentName] = true; + } + } + + if (workInProgress.mode & StrictLegacyMode) { + ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); + } + + setIsRendering(true); + ReactCurrentOwner.current = workInProgress; + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + setIsRendering(false); + } else { + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + } + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + + if (__DEV__) { + // Support for module components is deprecated and is removed behind a flag. + // Whether or not it would crash later, we want to show a good message in DEV first. + if ( + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + } + + if ( + // Run these checks in production only if the flag is off. + // Eventually we'll delete this branch altogether. + !disableModulePatternComponents && + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + if (__DEV__) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + + // Proceed under the assumption that this is a class instance + workInProgress.tag = ClassComponent; + + // Throw out any hooks that were used. + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + + // Push context providers early to prevent context stack mismatches. + // During mounting we don't know the child context yet as the instance doesn't exist. + // We will invalidate the child context in finishClassComponent() right after rendering. + let hasContext = false; + if (isLegacyContextProvider(Component)) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } + + workInProgress.memoizedState = + value.state !== null && value.state !== undefined ? value.state : null; + + initializeUpdateQueue(workInProgress); + + adoptClassInstance(workInProgress, value); + mountClassInstance(workInProgress, Component, props, renderLanes); + return finishClassComponent( + null, + workInProgress, + Component, + true, + hasContext, + renderLanes, + ); + } else { + // Proceed under the assumption that this is a function component + workInProgress.tag = FunctionComponent; + if (__DEV__) { + if (disableLegacyContext && Component.contextTypes) { + console.error( + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with React.useContext() instead.', + getComponentNameFromType(Component) || 'Unknown', + ); + } + } + + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + + reconcileChildren(null, workInProgress, value, renderLanes); + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, Component); + } + return workInProgress.child; + } +} + +function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { + if (__DEV__) { + if (Component) { + if (Component.childContextTypes) { + console.error( + '%s(...): childContextTypes cannot be defined on a function component.', + Component.displayName || Component.name || 'Component', + ); + } + } + if (workInProgress.ref !== null) { + let info = ''; + const ownerName = getCurrentFiberOwnerNameInDevOrNull(); + if (ownerName) { + info += '\n\nCheck the render method of `' + ownerName + '`.'; + } + + let warningKey = ownerName || ''; + const debugSource = workInProgress._debugSource; + if (debugSource) { + warningKey = debugSource.fileName + ':' + debugSource.lineNumber; + } + if (!didWarnAboutFunctionRefs[warningKey]) { + didWarnAboutFunctionRefs[warningKey] = true; + console.error( + 'Function components cannot be given refs. ' + + 'Attempts to access this ref will fail. ' + + 'Did you mean to use React.forwardRef()?%s', + info, + ); + } + } + + if ( + warnAboutDefaultPropsOnFunctionComponents && + Component.defaultProps !== undefined + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + + if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { + console.error( + '%s: Support for defaultProps will be removed from function components ' + + 'in a future major release. Use JavaScript default parameters instead.', + componentName, + ); + didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; + } + } + + if (typeof Component.getDerivedStateFromProps === 'function') { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + + if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) { + console.error( + '%s: Function components do not support getDerivedStateFromProps.', + componentName, + ); + didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true; + } + } + + if ( + typeof Component.contextType === 'object' && + Component.contextType !== null + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + + if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) { + console.error( + '%s: Function components do not support contextType.', + componentName, + ); + didWarnAboutContextTypeOnFunctionComponent[componentName] = true; + } + } + } +} + +const SUSPENDED_MARKER: SuspenseState = { + dehydrated: null, + treeContext: null, + retryLane: NoLane, +}; + +function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState { + return { + baseLanes: renderLanes, + cachePool: getSuspendedCache(), + }; +} + +function updateSuspenseOffscreenState( + prevOffscreenState: OffscreenState, + renderLanes: Lanes, +): OffscreenState { + let cachePool: SpawnedCachePool | null = null; + if (enableCache) { + const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool; + if (prevCachePool !== null) { + const parentCache = isPrimaryRenderer + ? CacheContext._currentValue + : CacheContext._currentValue2; + if (prevCachePool.parent !== parentCache) { + // Detected a refresh in the parent. This overrides any previously + // suspended cache. + cachePool = { + parent: parentCache, + pool: parentCache, + }; + } else { + // We can reuse the cache from last time. The only thing that would have + // overridden it is a parent refresh, which we checked for above. + cachePool = prevCachePool; + } + } else { + // If there's no previous cache pool, grab the current one. + cachePool = getSuspendedCache(); + } + } + return { + baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes), + cachePool, + }; +} + +// TODO: Probably should inline this back +function shouldRemainOnFallback( + current: null | Fiber, + workInProgress: Fiber, + renderLanes: Lanes, +) { + // If we're already showing a fallback, there are cases where we need to + // remain on that fallback regardless of whether the content has resolved. + // For example, SuspenseList coordinates when nested content appears. + if (current !== null) { + const suspenseState: SuspenseState = current.memoizedState; + if (suspenseState === null) { + // Currently showing content. Don't hide it, even if ForceSuspenseFallback + // is true. More precise name might be "ForceRemainSuspenseFallback". + // Note: This is a factoring smell. Can't remain on a fallback if there's + // no fallback to remain on. + return false; + } + } + + // Not currently showing content. Consult the Suspense context. + const suspenseContext: SuspenseContext = suspenseStackCursor.current; + return hasSuspenseListContext( + suspenseContext, + (ForceSuspenseFallback: SuspenseContext), + ); +} + +function getRemainingWorkInPrimaryTree(current: Fiber, renderLanes) { + // TODO: Should not remove render lanes that were pinged during this render + return removeLanes(current.childLanes, renderLanes); +} + +function updateSuspenseComponent(current, workInProgress, renderLanes) { + const nextProps = workInProgress.pendingProps; + + // This is used by DevTools to force a boundary to suspend. + if (__DEV__) { + if (shouldSuspend(workInProgress)) { + workInProgress.flags |= DidCapture; + } + } + + let showFallback = false; + const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags; + if ( + didSuspend || + shouldRemainOnFallback(current, workInProgress, renderLanes) + ) { + // Something in this boundary's subtree already suspended. Switch to + // rendering the fallback children. + showFallback = true; + workInProgress.flags &= ~DidCapture; + } + + // OK, the next part is confusing. We're about to reconcile the Suspense + // boundary's children. This involves some custom reconciliation logic. Two + // main reasons this is so complicated. + // + // First, Legacy Mode has different semantics for backwards compatibility. The + // primary tree will commit in an inconsistent state, so when we do the + // second pass to render the fallback, we do some exceedingly, uh, clever + // hacks to make that not totally break. Like transferring effects and + // deletions from hidden tree. In Concurrent Mode, it's much simpler, + // because we bailout on the primary tree completely and leave it in its old + // state, no effects. Same as what we do for Offscreen (except that + // Offscreen doesn't have the first render pass). + // + // Second is hydration. During hydration, the Suspense fiber has a slightly + // different layout, where the child points to a dehydrated fragment, which + // contains the DOM rendered by the server. + // + // Third, even if you set all that aside, Suspense is like error boundaries in + // that we first we try to render one tree, and if that fails, we render again + // and switch to a different tree. Like a try/catch block. So we have to track + // which branch we're currently rendering. Ideally we would model this using + // a stack. + if (current === null) { + // Initial mount + + // Special path for hydration + // If we're currently hydrating, try to hydrate this boundary. + if (getIsHydrating()) { + // We must push the suspense handler context *before* attempting to + // hydrate, to avoid a mismatch in case it errors. + if (showFallback) { + pushPrimaryTreeSuspenseHandler(workInProgress); + } else { + pushFallbackTreeSuspenseHandler(workInProgress); + } + tryToClaimNextHydratableInstance(workInProgress); + // This could've been a dehydrated suspense component. + const suspenseState: null | SuspenseState = workInProgress.memoizedState; + if (suspenseState !== null) { + const dehydrated = suspenseState.dehydrated; + if (dehydrated !== null) { + return mountDehydratedSuspenseComponent( + workInProgress, + dehydrated, + renderLanes, + ); + } + } + // If hydration didn't succeed, fall through to the normal Suspense path. + // To avoid a stack mismatch we need to pop the Suspense handler that we + // pushed above. This will become less awkward when move the hydration + // logic to its own fiber. + popSuspenseHandler(workInProgress); + } + + const nextPrimaryChildren = nextProps.children; + const nextFallbackChildren = nextProps.fallback; + + if (showFallback) { + pushFallbackTreeSuspenseHandler(workInProgress); + + const fallbackFragment = mountSuspenseFallbackChildren( + workInProgress, + nextPrimaryChildren, + nextFallbackChildren, + renderLanes, + ); + const primaryChildFragment: Fiber = (workInProgress.child: any); + primaryChildFragment.memoizedState = mountSuspenseOffscreenState( + renderLanes, + ); + workInProgress.memoizedState = SUSPENDED_MARKER; + if (enableTransitionTracing) { + const currentTransitions = getPendingTransitions(); + if (currentTransitions !== null) { + const parentMarkerInstances = getMarkerInstances(); + const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any); + if (offscreenQueue === null) { + const newOffscreenQueue: OffscreenQueue = { + transitions: currentTransitions, + markerInstances: parentMarkerInstances, + wakeables: null, + }; + primaryChildFragment.updateQueue = newOffscreenQueue; + } else { + offscreenQueue.transitions = currentTransitions; + offscreenQueue.markerInstances = parentMarkerInstances; + } + } + } + + return fallbackFragment; + } else if ( + enableCPUSuspense && + typeof nextProps.unstable_expectedLoadTime === 'number' + ) { + // This is a CPU-bound tree. Skip this tree and show a placeholder to + // unblock the surrounding content. Then immediately retry after the + // initial commit. + pushFallbackTreeSuspenseHandler(workInProgress); + const fallbackFragment = mountSuspenseFallbackChildren( + workInProgress, + nextPrimaryChildren, + nextFallbackChildren, + renderLanes, + ); + const primaryChildFragment: Fiber = (workInProgress.child: any); + primaryChildFragment.memoizedState = mountSuspenseOffscreenState( + renderLanes, + ); + workInProgress.memoizedState = SUSPENDED_MARKER; + + // TODO: Transition Tracing is not yet implemented for CPU Suspense. + + // Since nothing actually suspended, there will nothing to ping this to + // get it started back up to attempt the next item. While in terms of + // priority this work has the same priority as this current render, it's + // not part of the same transition once the transition has committed. If + // it's sync, we still want to yield so that it can be painted. + // Conceptually, this is really the same as pinging. We can use any + // RetryLane even if it's the one currently rendering since we're leaving + // it behind on this node. + workInProgress.lanes = SomeRetryLane; + return fallbackFragment; + } else { + pushPrimaryTreeSuspenseHandler(workInProgress); + return mountSuspensePrimaryChildren( + workInProgress, + nextPrimaryChildren, + renderLanes, + ); + } + } else { + // This is an update. + + // Special path for hydration + const prevState: null | SuspenseState = current.memoizedState; + if (prevState !== null) { + const dehydrated = prevState.dehydrated; + if (dehydrated !== null) { + return updateDehydratedSuspenseComponent( + current, + workInProgress, + didSuspend, + nextProps, + dehydrated, + prevState, + renderLanes, + ); + } + } + + if (showFallback) { + pushFallbackTreeSuspenseHandler(workInProgress); + + const nextFallbackChildren = nextProps.fallback; + const nextPrimaryChildren = nextProps.children; + const fallbackChildFragment = updateSuspenseFallbackChildren( + current, + workInProgress, + nextPrimaryChildren, + nextFallbackChildren, + renderLanes, + ); + const primaryChildFragment: Fiber = (workInProgress.child: any); + const prevOffscreenState: OffscreenState | null = (current.child: any) + .memoizedState; + primaryChildFragment.memoizedState = + prevOffscreenState === null + ? mountSuspenseOffscreenState(renderLanes) + : updateSuspenseOffscreenState(prevOffscreenState, renderLanes); + if (enableTransitionTracing) { + const currentTransitions = getPendingTransitions(); + if (currentTransitions !== null) { + const parentMarkerInstances = getMarkerInstances(); + const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any); + const currentOffscreenQueue: OffscreenQueue | null = (current.updateQueue: any); + if (offscreenQueue === null) { + const newOffscreenQueue: OffscreenQueue = { + transitions: currentTransitions, + markerInstances: parentMarkerInstances, + wakeables: null, + }; + primaryChildFragment.updateQueue = newOffscreenQueue; + } else if (offscreenQueue === currentOffscreenQueue) { + // If the work-in-progress queue is the same object as current, we + // can't modify it without cloning it first. + const newOffscreenQueue: OffscreenQueue = { + transitions: currentTransitions, + markerInstances: parentMarkerInstances, + wakeables: + currentOffscreenQueue !== null + ? currentOffscreenQueue.wakeables + : null, + }; + primaryChildFragment.updateQueue = newOffscreenQueue; + } else { + offscreenQueue.transitions = currentTransitions; + offscreenQueue.markerInstances = parentMarkerInstances; + } + } + } + primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree( + current, + renderLanes, + ); + workInProgress.memoizedState = SUSPENDED_MARKER; + return fallbackChildFragment; + } else { + pushPrimaryTreeSuspenseHandler(workInProgress); + + const nextPrimaryChildren = nextProps.children; + const primaryChildFragment = updateSuspensePrimaryChildren( + current, + workInProgress, + nextPrimaryChildren, + renderLanes, + ); + workInProgress.memoizedState = null; + return primaryChildFragment; + } + } +} + +function mountSuspensePrimaryChildren( + workInProgress, + primaryChildren, + renderLanes, +) { + const mode = workInProgress.mode; + const primaryChildProps: OffscreenProps = { + mode: 'visible', + children: primaryChildren, + }; + const primaryChildFragment = mountWorkInProgressOffscreenFiber( + primaryChildProps, + mode, + renderLanes, + ); + primaryChildFragment.return = workInProgress; + workInProgress.child = primaryChildFragment; + return primaryChildFragment; +} + +function mountSuspenseFallbackChildren( + workInProgress, + primaryChildren, + fallbackChildren, + renderLanes, +) { + const mode = workInProgress.mode; + const progressedPrimaryFragment: Fiber | null = workInProgress.child; + + const primaryChildProps: OffscreenProps = { + mode: 'hidden', + children: primaryChildren, + }; + + let primaryChildFragment; + let fallbackChildFragment; + if ( + (mode & ConcurrentMode) === NoMode && + progressedPrimaryFragment !== null + ) { + // In legacy mode, we commit the primary tree as if it successfully + // completed, even though it's in an inconsistent state. + primaryChildFragment = progressedPrimaryFragment; + primaryChildFragment.childLanes = NoLanes; + primaryChildFragment.pendingProps = primaryChildProps; + + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // Reset the durations from the first pass so they aren't included in the + // final amounts. This seems counterintuitive, since we're intentionally + // not measuring part of the render phase, but this makes it match what we + // do in Concurrent Mode. + primaryChildFragment.actualDuration = 0; + primaryChildFragment.actualStartTime = -1; + primaryChildFragment.selfBaseDuration = 0; + primaryChildFragment.treeBaseDuration = 0; + } + + fallbackChildFragment = createFiberFromFragment( + fallbackChildren, + mode, + renderLanes, + null, + ); + } else { + primaryChildFragment = mountWorkInProgressOffscreenFiber( + primaryChildProps, + mode, + NoLanes, + ); + fallbackChildFragment = createFiberFromFragment( + fallbackChildren, + mode, + renderLanes, + null, + ); + } + + primaryChildFragment.return = workInProgress; + fallbackChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + workInProgress.child = primaryChildFragment; + return fallbackChildFragment; +} + +function mountWorkInProgressOffscreenFiber( + offscreenProps: OffscreenProps, + mode: TypeOfMode, + renderLanes: Lanes, +) { + // The props argument to `createFiberFromOffscreen` is `any` typed, so we use + // this wrapper function to constrain it. + return createFiberFromOffscreen(offscreenProps, mode, NoLanes, null); +} + +function updateWorkInProgressOffscreenFiber( + current: Fiber, + offscreenProps: OffscreenProps, +) { + // The props argument to `createWorkInProgress` is `any` typed, so we use this + // wrapper function to constrain it. + return createWorkInProgress(current, offscreenProps); +} + +function updateSuspensePrimaryChildren( + current, + workInProgress, + primaryChildren, + renderLanes, +) { + const currentPrimaryChildFragment: Fiber = (current.child: any); + const currentFallbackChildFragment: Fiber | null = + currentPrimaryChildFragment.sibling; + + const primaryChildFragment = updateWorkInProgressOffscreenFiber( + currentPrimaryChildFragment, + { + mode: 'visible', + children: primaryChildren, + }, + ); + if ((workInProgress.mode & ConcurrentMode) === NoMode) { + primaryChildFragment.lanes = renderLanes; + } + primaryChildFragment.return = workInProgress; + primaryChildFragment.sibling = null; + if (currentFallbackChildFragment !== null) { + // Delete the fallback child fragment + const deletions = workInProgress.deletions; + if (deletions === null) { + workInProgress.deletions = [currentFallbackChildFragment]; + workInProgress.flags |= ChildDeletion; + } else { + deletions.push(currentFallbackChildFragment); + } + } + + workInProgress.child = primaryChildFragment; + return primaryChildFragment; +} + +function updateSuspenseFallbackChildren( + current, + workInProgress, + primaryChildren, + fallbackChildren, + renderLanes, +) { + const mode = workInProgress.mode; + const currentPrimaryChildFragment: Fiber = (current.child: any); + const currentFallbackChildFragment: Fiber | null = + currentPrimaryChildFragment.sibling; + + const primaryChildProps: OffscreenProps = { + mode: 'hidden', + children: primaryChildren, + }; + + let primaryChildFragment; + if ( + // In legacy mode, we commit the primary tree as if it successfully + // completed, even though it's in an inconsistent state. + (mode & ConcurrentMode) === NoMode && + // Make sure we're on the second pass, i.e. the primary child fragment was + // already cloned. In legacy mode, the only case where this isn't true is + // when DevTools forces us to display a fallback; we skip the first render + // pass entirely and go straight to rendering the fallback. (In Concurrent + // Mode, SuspenseList can also trigger this scenario, but this is a legacy- + // only codepath.) + workInProgress.child !== currentPrimaryChildFragment + ) { + const progressedPrimaryFragment: Fiber = (workInProgress.child: any); + primaryChildFragment = progressedPrimaryFragment; + primaryChildFragment.childLanes = NoLanes; + primaryChildFragment.pendingProps = primaryChildProps; + + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // Reset the durations from the first pass so they aren't included in the + // final amounts. This seems counterintuitive, since we're intentionally + // not measuring part of the render phase, but this makes it match what we + // do in Concurrent Mode. + primaryChildFragment.actualDuration = 0; + primaryChildFragment.actualStartTime = -1; + primaryChildFragment.selfBaseDuration = + currentPrimaryChildFragment.selfBaseDuration; + primaryChildFragment.treeBaseDuration = + currentPrimaryChildFragment.treeBaseDuration; + } + + // The fallback fiber was added as a deletion during the first pass. + // However, since we're going to remain on the fallback, we no longer want + // to delete it. + workInProgress.deletions = null; + } else { + primaryChildFragment = updateWorkInProgressOffscreenFiber( + currentPrimaryChildFragment, + primaryChildProps, + ); + // Since we're reusing a current tree, we need to reuse the flags, too. + // (We don't do this in legacy mode, because in legacy mode we don't re-use + // the current tree; see previous branch.) + primaryChildFragment.subtreeFlags = + currentPrimaryChildFragment.subtreeFlags & StaticMask; + } + let fallbackChildFragment; + if (currentFallbackChildFragment !== null) { + fallbackChildFragment = createWorkInProgress( + currentFallbackChildFragment, + fallbackChildren, + ); + } else { + fallbackChildFragment = createFiberFromFragment( + fallbackChildren, + mode, + renderLanes, + null, + ); + // Needs a placement effect because the parent (the Suspense boundary) already + // mounted but this is a new fiber. + fallbackChildFragment.flags |= Placement; + } + + fallbackChildFragment.return = workInProgress; + primaryChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + workInProgress.child = primaryChildFragment; + + return fallbackChildFragment; +} + +function retrySuspenseComponentWithoutHydrating( + current: Fiber, + workInProgress: Fiber, + renderLanes: Lanes, + recoverableError: CapturedValue | null, +) { + // Falling back to client rendering. Because this has performance + // implications, it's considered a recoverable error, even though the user + // likely won't observe anything wrong with the UI. + // + // The error is passed in as an argument to enforce that every caller provide + // a custom message, or explicitly opt out (currently the only path that opts + // out is legacy mode; every concurrent path provides an error). + if (recoverableError !== null) { + queueHydrationError(recoverableError); + } + + // This will add the old fiber to the deletion list + reconcileChildFibers(workInProgress, current.child, null, renderLanes); + + // We're now not suspended nor dehydrated. + const nextProps = workInProgress.pendingProps; + const primaryChildren = nextProps.children; + const primaryChildFragment = mountSuspensePrimaryChildren( + workInProgress, + primaryChildren, + renderLanes, + ); + // Needs a placement effect because the parent (the Suspense boundary) already + // mounted but this is a new fiber. + primaryChildFragment.flags |= Placement; + workInProgress.memoizedState = null; + + return primaryChildFragment; +} + +function mountSuspenseFallbackAfterRetryWithoutHydrating( + current, + workInProgress, + primaryChildren, + fallbackChildren, + renderLanes, +) { + const fiberMode = workInProgress.mode; + const primaryChildProps: OffscreenProps = { + mode: 'visible', + children: primaryChildren, + }; + const primaryChildFragment = mountWorkInProgressOffscreenFiber( + primaryChildProps, + fiberMode, + NoLanes, + ); + const fallbackChildFragment = createFiberFromFragment( + fallbackChildren, + fiberMode, + renderLanes, + null, + ); + // Needs a placement effect because the parent (the Suspense + // boundary) already mounted but this is a new fiber. + fallbackChildFragment.flags |= Placement; + + primaryChildFragment.return = workInProgress; + fallbackChildFragment.return = workInProgress; + primaryChildFragment.sibling = fallbackChildFragment; + workInProgress.child = primaryChildFragment; + + if ((workInProgress.mode & ConcurrentMode) !== NoMode) { + // We will have dropped the effect list which contains the + // deletion. We need to reconcile to delete the current child. + reconcileChildFibers(workInProgress, current.child, null, renderLanes); + } + + return fallbackChildFragment; +} + +function mountDehydratedSuspenseComponent( + workInProgress: Fiber, + suspenseInstance: SuspenseInstance, + renderLanes: Lanes, +): null | Fiber { + // During the first pass, we'll bail out and not drill into the children. + // Instead, we'll leave the content in place and try to hydrate it later. + if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if (__DEV__) { + console.error( + 'Cannot hydrate Suspense in legacy mode. Switch from ' + + 'ReactDOM.hydrate(element, container) to ' + + 'ReactDOMClient.hydrateRoot(container, )' + + '.render(element) or remove the Suspense components from ' + + 'the server rendered components.', + ); + } + workInProgress.lanes = laneToLanes(SyncLane); + } else if (isSuspenseInstanceFallback(suspenseInstance)) { + // This is a client-only boundary. Since we won't get any content from the server + // for this, we need to schedule that at a higher priority based on when it would + // have timed out. In theory we could render it in this pass but it would have the + // wrong priority associated with it and will prevent hydration of parent path. + // Instead, we'll leave work left on it to render it in a separate commit. + + // TODO This time should be the time at which the server rendered response that is + // a parent to this boundary was displayed. However, since we currently don't have + // a protocol to transfer that time, we'll just estimate it by using the current + // time. This will mean that Suspense timeouts are slightly shifted to later than + // they should be. + // Schedule a normal pri update to render this content. + workInProgress.lanes = laneToLanes(DefaultHydrationLane); + } else { + // We'll continue hydrating the rest at offscreen priority since we'll already + // be showing the right content coming from the server, it is no rush. + workInProgress.lanes = laneToLanes(OffscreenLane); + } + return null; +} + +function updateDehydratedSuspenseComponent( + current: Fiber, + workInProgress: Fiber, + didSuspend: boolean, + nextProps: any, + suspenseInstance: SuspenseInstance, + suspenseState: SuspenseState, + renderLanes: Lanes, +): null | Fiber { + if (!didSuspend) { + // This is the first render pass. Attempt to hydrate. + pushPrimaryTreeSuspenseHandler(workInProgress); + + // We should never be hydrating at this point because it is the first pass, + // but after we've already committed once. + warnIfHydrating(); + + if ((workInProgress.mode & ConcurrentMode) === NoMode) { + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + null, + ); + } + + if (isSuspenseInstanceFallback(suspenseInstance)) { + // This boundary is in a permanent fallback state. In this case, we'll never + // get an update and we'll never be able to hydrate the final content. Let's just try the + // client side render instead. + let digest, message, stack; + if (__DEV__) { + ({digest, message, stack} = getSuspenseInstanceFallbackErrorDetails( + suspenseInstance, + )); + } else { + ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance)); + } + + let error; + if (message) { + // eslint-disable-next-line react-internal/prod-error-codes + error = new Error(message); + } else { + error = new Error( + 'The server could not finish this Suspense boundary, likely ' + + 'due to an error during server rendering. Switched to ' + + 'client rendering.', + ); + } + (error: any).digest = digest; + const capturedValue = createCapturedValue(error, digest, stack); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + capturedValue, + ); + } + + if ( + enableLazyContextPropagation && + // TODO: Factoring is a little weird, since we check this right below, too. + // But don't want to re-arrange the if-else chain until/unless this + // feature lands. + !didReceiveUpdate + ) { + // We need to check if any children have context before we decide to bail + // out, so propagate the changes now. + lazilyPropagateParentContextChanges(current, workInProgress, renderLanes); + } + + // We use lanes to indicate that a child might depend on context, so if + // any context has changed, we need to treat is as if the input might have changed. + const hasContextChanged = includesSomeLane(renderLanes, current.childLanes); + if (didReceiveUpdate || hasContextChanged) { + // This boundary has changed since the first render. This means that we are now unable to + // hydrate it. We might still be able to hydrate it using a higher priority lane. + const root = getWorkInProgressRoot(); + if (root !== null) { + const attemptHydrationAtLane = getBumpedLaneForHydration( + root, + renderLanes, + ); + if ( + attemptHydrationAtLane !== NoLane && + attemptHydrationAtLane !== suspenseState.retryLane + ) { + // Intentionally mutating since this render will get interrupted. This + // is one of the very rare times where we mutate the current tree + // during the render phase. + suspenseState.retryLane = attemptHydrationAtLane; + // TODO: Ideally this would inherit the event time of the current render + const eventTime = NoTimestamp; + enqueueConcurrentRenderForLane(current, attemptHydrationAtLane); + scheduleUpdateOnFiber( + root, + current, + attemptHydrationAtLane, + eventTime, + ); + + // Throw a special object that signals to the work loop that it should + // interrupt the current render. + // + // Because we're inside a React-only execution stack, we don't + // strictly need to throw here — we could instead modify some internal + // work loop state. But using an exception means we don't need to + // check for this case on every iteration of the work loop. So doing + // it this way moves the check out of the fast path. + throw SelectiveHydrationException; + } else { + // We have already tried to ping at a higher priority than we're rendering with + // so if we got here, we must have failed to hydrate at those levels. We must + // now give up. Instead, we're going to delete the whole subtree and instead inject + // a new real Suspense boundary to take its place, which may render content + // or fallback. This might suspend for a while and if it does we might still have + // an opportunity to hydrate before this pass commits. + } + } + + // If we did not selectively hydrate, we'll continue rendering without + // hydrating. Mark this tree as suspended to prevent it from committing + // outside a transition. + // + // This path should only happen if the hydration lane already suspended. + // Currently, it also happens during sync updates because there is no + // hydration lane for sync updates. + // TODO: We should ideally have a sync hydration lane that we can apply to do + // a pass where we hydrate this subtree in place using the previous Context and then + // reapply the update afterwards. + renderDidSuspendDelayIfPossible(); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + null, + ); + } else if (isSuspenseInstancePending(suspenseInstance)) { + // This component is still pending more data from the server, so we can't hydrate its + // content. We treat it as if this component suspended itself. It might seem as if + // we could just try to render it client-side instead. However, this will perform a + // lot of unnecessary work and is unlikely to complete since it often will suspend + // on missing data anyway. Additionally, the server might be able to render more + // than we can on the client yet. In that case we'd end up with more fallback states + // on the client than if we just leave it alone. If the server times out or errors + // these should update this boundary to the permanent Fallback state instead. + // Mark it as having captured (i.e. suspended). + workInProgress.flags |= DidCapture; + // Leave the child in place. I.e. the dehydrated fragment. + workInProgress.child = current.child; + // Register a callback to retry this boundary once the server has sent the result. + const retry = retryDehydratedSuspenseBoundary.bind(null, current); + registerSuspenseInstanceRetry(suspenseInstance, retry); + return null; + } else { + // This is the first attempt. + reenterHydrationStateFromDehydratedSuspenseInstance( + workInProgress, + suspenseInstance, + suspenseState.treeContext, + ); + const primaryChildren = nextProps.children; + const primaryChildFragment = mountSuspensePrimaryChildren( + workInProgress, + primaryChildren, + renderLanes, + ); + // Mark the children as hydrating. This is a fast path to know whether this + // tree is part of a hydrating tree. This is used to determine if a child + // node has fully mounted yet, and for scheduling event replaying. + // Conceptually this is similar to Placement in that a new subtree is + // inserted into the React tree here. It just happens to not need DOM + // mutations because it already exists. + primaryChildFragment.flags |= Hydrating; + return primaryChildFragment; + } + } else { + // This is the second render pass. We already attempted to hydrated, but + // something either suspended or errored. + + if (workInProgress.flags & ForceClientRender) { + // Something errored during hydration. Try again without hydrating. + pushPrimaryTreeSuspenseHandler(workInProgress); + + workInProgress.flags &= ~ForceClientRender; + const capturedValue = createCapturedValue( + new Error( + 'There was an error while hydrating this Suspense boundary. ' + + 'Switched to client rendering.', + ), + ); + return retrySuspenseComponentWithoutHydrating( + current, + workInProgress, + renderLanes, + capturedValue, + ); + } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) { + // Something suspended and we should still be in dehydrated mode. + // Leave the existing child in place. + + // Push to avoid a mismatch + pushFallbackTreeSuspenseHandler(workInProgress); + + workInProgress.child = current.child; + // The dehydrated completion pass expects this flag to be there + // but the normal suspense pass doesn't. + workInProgress.flags |= DidCapture; + return null; + } else { + // Suspended but we should no longer be in dehydrated mode. + // Therefore we now have to render the fallback. + pushFallbackTreeSuspenseHandler(workInProgress); + + const nextPrimaryChildren = nextProps.children; + const nextFallbackChildren = nextProps.fallback; + const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating( + current, + workInProgress, + nextPrimaryChildren, + nextFallbackChildren, + renderLanes, + ); + const primaryChildFragment: Fiber = (workInProgress.child: any); + primaryChildFragment.memoizedState = mountSuspenseOffscreenState( + renderLanes, + ); + workInProgress.memoizedState = SUSPENDED_MARKER; + return fallbackChildFragment; + } + } +} + +function scheduleSuspenseWorkOnFiber( + fiber: Fiber, + renderLanes: Lanes, + propagationRoot: Fiber, +) { + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); + } + scheduleContextWorkOnParentPath(fiber.return, renderLanes, propagationRoot); +} + +function propagateSuspenseContextChange( + workInProgress: Fiber, + firstChild: null | Fiber, + renderLanes: Lanes, +): void { + // Mark any Suspense boundaries with fallbacks as having work to do. + // If they were previously forced into fallbacks, they may now be able + // to unblock. + let node = firstChild; + while (node !== null) { + if (node.tag === SuspenseComponent) { + const state: SuspenseState | null = node.memoizedState; + if (state !== null) { + scheduleSuspenseWorkOnFiber(node, renderLanes, workInProgress); + } + } else if (node.tag === SuspenseListComponent) { + // If the tail is hidden there might not be an Suspense boundaries + // to schedule work on. In this case we have to schedule it on the + // list itself. + // We don't have to traverse to the children of the list since + // the list will propagate the change when it rerenders. + scheduleSuspenseWorkOnFiber(node, renderLanes, workInProgress); + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === workInProgress) { + return; + } + // $FlowFixMe[incompatible-use] found when upgrading Flow + while (node.sibling === null) { + // $FlowFixMe[incompatible-use] found when upgrading Flow + if (node.return === null || node.return === workInProgress) { + return; + } + node = node.return; + } + // $FlowFixMe[incompatible-use] found when upgrading Flow + node.sibling.return = node.return; + node = node.sibling; + } +} + +function findLastContentRow(firstChild: null | Fiber): null | Fiber { + // This is going to find the last row among these children that is already + // showing content on the screen, as opposed to being in fallback state or + // new. If a row has multiple Suspense boundaries, any of them being in the + // fallback state, counts as the whole row being in a fallback state. + // Note that the "rows" will be workInProgress, but any nested children + // will still be current since we haven't rendered them yet. The mounted + // order may not be the same as the new order. We use the new order. + let row = firstChild; + let lastContentRow: null | Fiber = null; + while (row !== null) { + const currentRow = row.alternate; + // New rows can't be content rows. + if (currentRow !== null && findFirstSuspended(currentRow) === null) { + lastContentRow = row; + } + row = row.sibling; + } + return lastContentRow; +} + +type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void; + +function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { + if (__DEV__) { + if ( + revealOrder !== undefined && + revealOrder !== 'forwards' && + revealOrder !== 'backwards' && + revealOrder !== 'together' && + !didWarnAboutRevealOrder[revealOrder] + ) { + didWarnAboutRevealOrder[revealOrder] = true; + if (typeof revealOrder === 'string') { + switch (revealOrder.toLowerCase()) { + case 'together': + case 'forwards': + case 'backwards': { + console.error( + '"%s" is not a valid value for revealOrder on . ' + + 'Use lowercase "%s" instead.', + revealOrder, + revealOrder.toLowerCase(), + ); + break; + } + case 'forward': + case 'backward': { + console.error( + '"%s" is not a valid value for revealOrder on . ' + + 'React uses the -s suffix in the spelling. Use "%ss" instead.', + revealOrder, + revealOrder.toLowerCase(), + ); + break; + } + default: + console.error( + '"%s" is not a supported revealOrder on . ' + + 'Did you mean "together", "forwards" or "backwards"?', + revealOrder, + ); + break; + } + } else { + console.error( + '%s is not a supported value for revealOrder on . ' + + 'Did you mean "together", "forwards" or "backwards"?', + revealOrder, + ); + } + } + } +} + +function validateTailOptions( + tailMode: SuspenseListTailMode, + revealOrder: SuspenseListRevealOrder, +) { + if (__DEV__) { + if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) { + if (tailMode !== 'collapsed' && tailMode !== 'hidden') { + didWarnAboutTailOptions[tailMode] = true; + console.error( + '"%s" is not a supported value for tail on . ' + + 'Did you mean "collapsed" or "hidden"?', + tailMode, + ); + } else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') { + didWarnAboutTailOptions[tailMode] = true; + console.error( + ' is only valid if revealOrder is ' + + '"forwards" or "backwards". ' + + 'Did you mean to specify revealOrder="forwards"?', + tailMode, + ); + } + } + } +} + +function validateSuspenseListNestedChild(childSlot: mixed, index: number) { + if (__DEV__) { + const isAnArray = isArray(childSlot); + const isIterable = + !isAnArray && typeof getIteratorFn(childSlot) === 'function'; + if (isAnArray || isIterable) { + const type = isAnArray ? 'array' : 'iterable'; + console.error( + 'A nested %s was passed to row #%s in . Wrap it in ' + + 'an additional SuspenseList to configure its revealOrder: ' + + ' ... ' + + '{%s} ... ' + + '', + type, + index, + type, + ); + return false; + } + } + return true; +} + +function validateSuspenseListChildren( + children: mixed, + revealOrder: SuspenseListRevealOrder, +) { + if (__DEV__) { + if ( + (revealOrder === 'forwards' || revealOrder === 'backwards') && + children !== undefined && + children !== null && + children !== false + ) { + if (isArray(children)) { + for (let i = 0; i < children.length; i++) { + if (!validateSuspenseListNestedChild(children[i], i)) { + return; + } + } + } else { + const iteratorFn = getIteratorFn(children); + if (typeof iteratorFn === 'function') { + const childrenIterator = iteratorFn.call(children); + if (childrenIterator) { + let step = childrenIterator.next(); + let i = 0; + for (; !step.done; step = childrenIterator.next()) { + if (!validateSuspenseListNestedChild(step.value, i)) { + return; + } + i++; + } + } + } else { + console.error( + 'A single row was passed to a . ' + + 'This is not useful since it needs multiple rows. ' + + 'Did you mean to pass multiple children or an array?', + revealOrder, + ); + } + } + } + } +} + +function initSuspenseListRenderState( + workInProgress: Fiber, + isBackwards: boolean, + tail: null | Fiber, + lastContentRow: null | Fiber, + tailMode: SuspenseListTailMode, +): void { + const renderState: null | SuspenseListRenderState = + workInProgress.memoizedState; + if (renderState === null) { + workInProgress.memoizedState = ({ + isBackwards: isBackwards, + rendering: null, + renderingStartTime: 0, + last: lastContentRow, + tail: tail, + tailMode: tailMode, + }: SuspenseListRenderState); + } else { + // We can reuse the existing object from previous renders. + renderState.isBackwards = isBackwards; + renderState.rendering = null; + renderState.renderingStartTime = 0; + renderState.last = lastContentRow; + renderState.tail = tail; + renderState.tailMode = tailMode; + } +} + +// This can end up rendering this component multiple passes. +// The first pass splits the children fibers into two sets. A head and tail. +// We first render the head. If anything is in fallback state, we do another +// pass through beginWork to rerender all children (including the tail) with +// the force suspend context. If the first render didn't have anything in +// in fallback state. Then we render each row in the tail one-by-one. +// That happens in the completeWork phase without going back to beginWork. +function updateSuspenseListComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + const nextProps = workInProgress.pendingProps; + const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder; + const tailMode: SuspenseListTailMode = nextProps.tail; + const newChildren = nextProps.children; + + validateRevealOrder(revealOrder); + validateTailOptions(tailMode, revealOrder); + validateSuspenseListChildren(newChildren, revealOrder); + + reconcileChildren(current, workInProgress, newChildren, renderLanes); + + let suspenseContext: SuspenseContext = suspenseStackCursor.current; + + const shouldForceFallback = hasSuspenseListContext( + suspenseContext, + (ForceSuspenseFallback: SuspenseContext), + ); + if (shouldForceFallback) { + suspenseContext = setShallowSuspenseListContext( + suspenseContext, + ForceSuspenseFallback, + ); + workInProgress.flags |= DidCapture; + } else { + const didSuspendBefore = + current !== null && (current.flags & DidCapture) !== NoFlags; + if (didSuspendBefore) { + // If we previously forced a fallback, we need to schedule work + // on any nested boundaries to let them know to try to render + // again. This is the same as context updating. + propagateSuspenseContextChange( + workInProgress, + workInProgress.child, + renderLanes, + ); + } + suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext); + } + pushSuspenseListContext(workInProgress, suspenseContext); + + if ((workInProgress.mode & ConcurrentMode) === NoMode) { + // In legacy mode, SuspenseList doesn't work so we just + // use make it a noop by treating it as the default revealOrder. + workInProgress.memoizedState = null; + } else { + switch (revealOrder) { + case 'forwards': { + const lastContentRow = findLastContentRow(workInProgress.child); + let tail; + if (lastContentRow === null) { + // The whole list is part of the tail. + // TODO: We could fast path by just rendering the tail now. + tail = workInProgress.child; + workInProgress.child = null; + } else { + // Disconnect the tail rows after the content row. + // We're going to render them separately later. + tail = lastContentRow.sibling; + lastContentRow.sibling = null; + } + initSuspenseListRenderState( + workInProgress, + false, // isBackwards + tail, + lastContentRow, + tailMode, + ); + break; + } + case 'backwards': { + // We're going to find the first row that has existing content. + // At the same time we're going to reverse the list of everything + // we pass in the meantime. That's going to be our tail in reverse + // order. + let tail = null; + let row = workInProgress.child; + workInProgress.child = null; + while (row !== null) { + const currentRow = row.alternate; + // New rows can't be content rows. + if (currentRow !== null && findFirstSuspended(currentRow) === null) { + // This is the beginning of the main content. + workInProgress.child = row; + break; + } + const nextRow = row.sibling; + row.sibling = tail; + tail = row; + row = nextRow; + } + // TODO: If workInProgress.child is null, we can continue on the tail immediately. + initSuspenseListRenderState( + workInProgress, + true, // isBackwards + tail, + null, // last + tailMode, + ); + break; + } + case 'together': { + initSuspenseListRenderState( + workInProgress, + false, // isBackwards + null, // tail + null, // last + undefined, + ); + break; + } + default: { + // The default reveal order is the same as not having + // a boundary. + workInProgress.memoizedState = null; + } + } + } + return workInProgress.child; +} + +function updatePortalComponent( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); + const nextChildren = workInProgress.pendingProps; + if (current === null) { + // Portals are special because we don't append the children during mount + // but at commit. Therefore we need to track insertions which the normal + // flow doesn't do during mount. This doesn't happen at the root because + // the root always starts with a "current" with a null child. + // TODO: Consider unifying this with how the root works. + workInProgress.child = reconcileChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); + } else { + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + } + return workInProgress.child; +} + +let hasWarnedAboutUsingNoValuePropOnContextProvider = false; + +function updateContextProvider( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + const providerType: ReactProviderType = workInProgress.type; + const context: ReactContext = providerType._context; + + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; + + const newValue = newProps.value; + + if (__DEV__) { + if (!('value' in newProps)) { + if (!hasWarnedAboutUsingNoValuePropOnContextProvider) { + hasWarnedAboutUsingNoValuePropOnContextProvider = true; + console.error( + 'The `value` prop is required for the ``. Did you misspell it or forget to pass it?', + ); + } + } + const providerPropTypes = workInProgress.type.propTypes; + + if (providerPropTypes) { + checkPropTypes(providerPropTypes, newProps, 'prop', 'Context.Provider'); + } + } + + pushProvider(workInProgress, context, newValue); + + if (enableLazyContextPropagation) { + // In the lazy propagation implementation, we don't scan for matching + // consumers until something bails out, because until something bails out + // we're going to visit those nodes, anyway. The trade-off is that it shifts + // responsibility to the consumer to track whether something has changed. + } else { + if (oldProps !== null) { + const oldValue = oldProps.value; + if (is(oldValue, newValue)) { + // No change. Bailout early if children are the same. + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderLanes, + ); + } + } else { + // The context value changed. Search for matching consumers and schedule + // them to update. + propagateContextChange(workInProgress, context, renderLanes); + } + } + } + + const newChildren = newProps.children; + reconcileChildren(current, workInProgress, newChildren, renderLanes); + return workInProgress.child; +} + +let hasWarnedAboutUsingContextAsConsumer = false; + +function updateContextConsumer( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +) { + let context: ReactContext = workInProgress.type; + // The logic below for Context differs depending on PROD or DEV mode. In + // DEV mode, we create a separate object for Context.Consumer that acts + // like a proxy to Context. This proxy object adds unnecessary code in PROD + // so we use the old behaviour (Context.Consumer references Context) to + // reduce size and overhead. The separate object references context via + // a property called "_context", which also gives us the ability to check + // in DEV mode if this property exists or not and warn if it does not. + if (__DEV__) { + if ((context: any)._context === undefined) { + // This may be because it's a Context (rather than a Consumer). + // Or it may be because it's older React where they're the same thing. + // We only want to warn if we're sure it's a new React. + if (context !== context.Consumer) { + if (!hasWarnedAboutUsingContextAsConsumer) { + hasWarnedAboutUsingContextAsConsumer = true; + console.error( + 'Rendering directly is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + } + } + } else { + context = (context: any)._context; + } + } + const newProps = workInProgress.pendingProps; + const render = newProps.children; + + if (__DEV__) { + if (typeof render !== 'function') { + console.error( + 'A context consumer was rendered with multiple children, or a child ' + + "that isn't a function. A context consumer expects a single child " + + 'that is a function. If you did pass a function, make sure there ' + + 'is no trailing or leading whitespace around it.', + ); + } + } + + prepareToReadContext(workInProgress, renderLanes); + const newValue = readContext(context); + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + let newChildren; + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setIsRendering(true); + newChildren = render(newValue); + setIsRendering(false); + } else { + newChildren = render(newValue); + } + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + reconcileChildren(current, workInProgress, newChildren, renderLanes); + return workInProgress.child; +} + +function updateScopeComponent(current, workInProgress, renderLanes) { + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; +} + +export function markWorkInProgressReceivedUpdate() { + didReceiveUpdate = true; +} + +export function checkIfWorkInProgressReceivedUpdate(): boolean { + return didReceiveUpdate; +} + +function resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress) { + if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if (current !== null) { + // A lazy component only mounts if it suspended inside a non- + // concurrent tree, in an inconsistent state. We want to treat it like + // a new mount, even though an empty version of it already committed. + // Disconnect the alternate pointers. + current.alternate = null; + workInProgress.alternate = null; + // Since this is conceptually a new fiber, schedule a Placement effect + workInProgress.flags |= Placement; + } + } +} + +function bailoutOnAlreadyFinishedWork( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + if (current !== null) { + // Reuse previous dependencies + workInProgress.dependencies = current.dependencies; + } + + if (enableProfilerTimer) { + // Don't update "base" render times for bailouts. + stopProfilerTimerIfRunning(workInProgress); + } + + markSkippedUpdateLanes(workInProgress.lanes); + + // Check if the children have any pending work. + if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { + // The children don't have any work either. We can skip them. + // TODO: Once we add back resuming, we should check if the children are + // a work-in-progress set. If so, we need to transfer their effects. + + if (enableLazyContextPropagation && current !== null) { + // Before bailing out, check if there are any context changes in + // the children. + lazilyPropagateParentContextChanges(current, workInProgress, renderLanes); + if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { + return null; + } + } else { + return null; + } + } + + // This fiber doesn't have work, but its subtree does. Clone the child + // fibers and continue. + cloneChildFibers(current, workInProgress); + return workInProgress.child; +} + +function remountFiber( + current: Fiber, + oldWorkInProgress: Fiber, + newWorkInProgress: Fiber, +): Fiber | null { + if (__DEV__) { + const returnFiber = oldWorkInProgress.return; + if (returnFiber === null) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error('Cannot swap the root fiber.'); + } + + // Disconnect from the old current. + // It will get deleted. + current.alternate = null; + oldWorkInProgress.alternate = null; + + // Connect to the new tree. + newWorkInProgress.index = oldWorkInProgress.index; + newWorkInProgress.sibling = oldWorkInProgress.sibling; + newWorkInProgress.return = oldWorkInProgress.return; + newWorkInProgress.ref = oldWorkInProgress.ref; + + // Replace the child/sibling pointers above it. + if (oldWorkInProgress === returnFiber.child) { + returnFiber.child = newWorkInProgress; + } else { + let prevSibling = returnFiber.child; + if (prevSibling === null) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error('Expected parent to have a child.'); + } + // $FlowFixMe[incompatible-use] found when upgrading Flow + while (prevSibling.sibling !== oldWorkInProgress) { + // $FlowFixMe[incompatible-use] found when upgrading Flow + prevSibling = prevSibling.sibling; + if (prevSibling === null) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error('Expected to find the previous sibling.'); + } + } + // $FlowFixMe[incompatible-use] found when upgrading Flow + prevSibling.sibling = newWorkInProgress; + } + + // Delete the old fiber and place the new one. + // Since the old fiber is disconnected, we have to schedule it manually. + const deletions = returnFiber.deletions; + if (deletions === null) { + returnFiber.deletions = [current]; + returnFiber.flags |= ChildDeletion; + } else { + deletions.push(current); + } + + newWorkInProgress.flags |= Placement; + + // Restart work from the new fiber. + return newWorkInProgress; + } else { + throw new Error( + 'Did not expect this call in production. ' + + 'This is a bug in React. Please file an issue.', + ); + } +} + +function checkScheduledUpdateOrContext( + current: Fiber, + renderLanes: Lanes, +): boolean { + // Before performing an early bailout, we must check if there are pending + // updates or context. + const updateLanes = current.lanes; + if (includesSomeLane(updateLanes, renderLanes)) { + return true; + } + // No pending update, but because context is propagated lazily, we need + // to check for a context change before we bail out. + if (enableLazyContextPropagation) { + const dependencies = current.dependencies; + if (dependencies !== null && checkIfContextChanged(dependencies)) { + return true; + } + } + return false; +} + +function attemptEarlyBailoutIfNoScheduledUpdate( + current: Fiber, + workInProgress: Fiber, + renderLanes: Lanes, +) { + // This fiber does not have any pending work. Bailout without entering + // the begin phase. There's still some bookkeeping we that needs to be done + // in this optimized path, mostly pushing stuff onto the stack. + switch (workInProgress.tag) { + case HostRoot: + pushHostRootContext(workInProgress); + const root: FiberRoot = workInProgress.stateNode; + pushRootTransition(workInProgress, root, renderLanes); + + if (enableTransitionTracing) { + pushRootMarkerInstance(workInProgress); + } + + if (enableCache) { + const cache: Cache = current.memoizedState.cache; + pushCacheProvider(workInProgress, cache); + } + resetHydrationState(); + break; + case HostResource: + case HostSingleton: + case HostComponent: + pushHostContext(workInProgress); + break; + case ClassComponent: { + const Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { + pushLegacyContextProvider(workInProgress); + } + break; + } + case HostPortal: + pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); + break; + case ContextProvider: { + const newValue = workInProgress.memoizedProps.value; + const context: ReactContext = workInProgress.type._context; + pushProvider(workInProgress, context, newValue); + break; + } + case Profiler: + if (enableProfilerTimer) { + // Profiler should only call onRender when one of its descendants actually rendered. + const hasChildWork = includesSomeLane( + renderLanes, + workInProgress.childLanes, + ); + if (hasChildWork) { + workInProgress.flags |= Update; + } + + if (enableProfilerCommitHooks) { + // Reset effect durations for the next eventual effect phase. + // These are reset during render to allow the DevTools commit hook a chance to read them, + const stateNode = workInProgress.stateNode; + stateNode.effectDuration = 0; + stateNode.passiveEffectDuration = 0; + } + } + break; + case SuspenseComponent: { + const state: SuspenseState | null = workInProgress.memoizedState; + if (state !== null) { + if (state.dehydrated !== null) { + // We're not going to render the children, so this is just to maintain + // push/pop symmetry + pushPrimaryTreeSuspenseHandler(workInProgress); + // We know that this component will suspend again because if it has + // been unsuspended it has committed as a resolved Suspense component. + // If it needs to be retried, it should have work scheduled on it. + workInProgress.flags |= DidCapture; + // We should never render the children of a dehydrated boundary until we + // upgrade it. We return null instead of bailoutOnAlreadyFinishedWork. + return null; + } + + // If this boundary is currently timed out, we need to decide + // whether to retry the primary children, or to skip over it and + // go straight to the fallback. Check the priority of the primary + // child fragment. + const primaryChildFragment: Fiber = (workInProgress.child: any); + const primaryChildLanes = primaryChildFragment.childLanes; + if (includesSomeLane(renderLanes, primaryChildLanes)) { + // The primary children have pending work. Use the normal path + // to attempt to render the primary children again. + return updateSuspenseComponent(current, workInProgress, renderLanes); + } else { + // The primary child fragment does not have pending work marked + // on it + pushPrimaryTreeSuspenseHandler(workInProgress); + // The primary children do not have pending work with sufficient + // priority. Bailout. + const child = bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderLanes, + ); + if (child !== null) { + // The fallback children have pending work. Skip over the + // primary children and work on the fallback. + return child.sibling; + } else { + // Note: We can return `null` here because we already checked + // whether there were nested context consumers, via the call to + // `bailoutOnAlreadyFinishedWork` above. + return null; + } + } + } else { + pushPrimaryTreeSuspenseHandler(workInProgress); + } + break; + } + case SuspenseListComponent: { + const didSuspendBefore = (current.flags & DidCapture) !== NoFlags; + + let hasChildWork = includesSomeLane( + renderLanes, + workInProgress.childLanes, + ); + + if (enableLazyContextPropagation && !hasChildWork) { + // Context changes may not have been propagated yet. We need to do + // that now, before we can decide whether to bail out. + // TODO: We use `childLanes` as a heuristic for whether there is + // remaining work in a few places, including + // `bailoutOnAlreadyFinishedWork` and + // `updateDehydratedSuspenseComponent`. We should maybe extract this + // into a dedicated function. + lazilyPropagateParentContextChanges( + current, + workInProgress, + renderLanes, + ); + hasChildWork = includesSomeLane(renderLanes, workInProgress.childLanes); + } + + if (didSuspendBefore) { + if (hasChildWork) { + // If something was in fallback state last time, and we have all the + // same children then we're still in progressive loading state. + // Something might get unblocked by state updates or retries in the + // tree which will affect the tail. So we need to use the normal + // path to compute the correct tail. + return updateSuspenseListComponent( + current, + workInProgress, + renderLanes, + ); + } + // If none of the children had any work, that means that none of + // them got retried so they'll still be blocked in the same way + // as before. We can fast bail out. + workInProgress.flags |= DidCapture; + } + + // If nothing suspended before and we're rendering the same children, + // then the tail doesn't matter. Anything new that suspends will work + // in the "together" mode, so we can continue from the state we had. + const renderState = workInProgress.memoizedState; + if (renderState !== null) { + // Reset to the "together" mode in case we've started a different + // update in the past but didn't complete it. + renderState.rendering = null; + renderState.tail = null; + renderState.lastEffect = null; + } + pushSuspenseListContext(workInProgress, suspenseStackCursor.current); + + if (hasChildWork) { + break; + } else { + // If none of the children had any work, that means that none of + // them got retried so they'll still be blocked in the same way + // as before. We can fast bail out. + return null; + } + } + case OffscreenComponent: + case LegacyHiddenComponent: { + // Need to check if the tree still needs to be deferred. This is + // almost identical to the logic used in the normal update path, + // so we'll just enter that. The only difference is we'll bail out + // at the next level instead of this one, because the child props + // have not changed. Which is fine. + // TODO: Probably should refactor `beginWork` to split the bailout + // path from the normal path. I'm tempted to do a labeled break here + // but I won't :) + workInProgress.lanes = NoLanes; + return updateOffscreenComponent(current, workInProgress, renderLanes); + } + case CacheComponent: { + if (enableCache) { + const cache: Cache = current.memoizedState.cache; + pushCacheProvider(workInProgress, cache); + } + break; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + const instance: TracingMarkerInstance | null = workInProgress.stateNode; + if (instance !== null) { + pushMarkerInstance(workInProgress, instance); + } + } + } + } + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); +} + +function beginWork( + current: Fiber | null, + workInProgress: Fiber, + renderLanes: Lanes, +): Fiber | null { + if (__DEV__) { + if (workInProgress._debugNeedsRemount && current !== null) { + // This will restart the begin phase with a new fiber. + return remountFiber( + current, + workInProgress, + createFiberFromTypeAndProps( + workInProgress.type, + workInProgress.key, + workInProgress.pendingProps, + workInProgress._debugOwner || null, + workInProgress.mode, + workInProgress.lanes, + ), + ); + } + } + + if (current !== null) { + const oldProps = current.memoizedProps; + const newProps = workInProgress.pendingProps; + + if ( + oldProps !== newProps || + hasLegacyContextChanged() || + // Force a re-render if the implementation changed due to hot reload: + (__DEV__ ? workInProgress.type !== current.type : false) + ) { + // If props or context changed, mark the fiber as having performed work. + // This may be unset if the props are determined to be equal later (memo). + didReceiveUpdate = true; + } else { + // Neither props nor legacy context changes. Check if there's a pending + // update or context change. + const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( + current, + renderLanes, + ); + if ( + !hasScheduledUpdateOrContext && + // If this is the second pass of an error or suspense boundary, there + // may not be work scheduled on `current`, so we check for this flag. + (workInProgress.flags & DidCapture) === NoFlags + ) { + // No pending updates or context. Bail out now. + didReceiveUpdate = false; + return attemptEarlyBailoutIfNoScheduledUpdate( + current, + workInProgress, + renderLanes, + ); + } + if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { + // This is a special case that only exists for legacy mode. + // See https://github.com/facebook/react/pull/19216. + didReceiveUpdate = true; + } else { + // An update was scheduled on this fiber, but there are no new props + // nor legacy context. Set this to false. If an update queue or context + // consumer produces a changed value, it will set this to true. Otherwise, + // the component will assume the children have not changed and bail out. + didReceiveUpdate = false; + } + } + } else { + didReceiveUpdate = false; + + if (getIsHydrating() && isForkedChild(workInProgress)) { + // Check if this child belongs to a list of muliple children in + // its parent. + // + // In a true multi-threaded implementation, we would render children on + // parallel threads. This would represent the beginning of a new render + // thread for this subtree. + // + // We only use this for id generation during hydration, which is why the + // logic is located in this special branch. + const slotIndex = workInProgress.index; + const numberOfForks = getForksAtLevel(workInProgress); + pushTreeId(workInProgress, numberOfForks, slotIndex); + } + } + + // Before entering the begin phase, clear pending update priority. + // TODO: This assumes that we're about to evaluate the component and process + // the update queue. However, there's an exception: SimpleMemoComponent + // sometimes bails out later in the begin phase. This indicates that we should + // move this assignment out of the common path and into each branch. + workInProgress.lanes = NoLanes; + + switch (workInProgress.tag) { + case IndeterminateComponent: { + return mountIndeterminateComponent( + current, + workInProgress, + workInProgress.type, + renderLanes, + ); + } + case LazyComponent: { + const elementType = workInProgress.elementType; + return mountLazyComponent( + current, + workInProgress, + elementType, + renderLanes, + ); + } + case FunctionComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === Component + ? unresolvedProps + : resolveDefaultProps(Component, unresolvedProps); + return updateFunctionComponent( + current, + workInProgress, + Component, + resolvedProps, + renderLanes, + ); + } + case ClassComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === Component + ? unresolvedProps + : resolveDefaultProps(Component, unresolvedProps); + return updateClassComponent( + current, + workInProgress, + Component, + resolvedProps, + renderLanes, + ); + } + case HostRoot: + return updateHostRoot(current, workInProgress, renderLanes); + case HostResource: + if (enableFloat && supportsResources) { + return updateHostResource(current, workInProgress, renderLanes); + } + // eslint-disable-next-line no-fallthrough + case HostSingleton: + if (enableHostSingletons && supportsSingletons) { + return updateHostSingleton(current, workInProgress, renderLanes); + } + // eslint-disable-next-line no-fallthrough + case HostComponent: + return updateHostComponent(current, workInProgress, renderLanes); + case HostText: + return updateHostText(current, workInProgress); + case SuspenseComponent: + return updateSuspenseComponent(current, workInProgress, renderLanes); + case HostPortal: + return updatePortalComponent(current, workInProgress, renderLanes); + case ForwardRef: { + const type = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === type + ? unresolvedProps + : resolveDefaultProps(type, unresolvedProps); + return updateForwardRef( + current, + workInProgress, + type, + resolvedProps, + renderLanes, + ); + } + case Fragment: + return updateFragment(current, workInProgress, renderLanes); + case Mode: + return updateMode(current, workInProgress, renderLanes); + case Profiler: + return updateProfiler(current, workInProgress, renderLanes); + case ContextProvider: + return updateContextProvider(current, workInProgress, renderLanes); + case ContextConsumer: + return updateContextConsumer(current, workInProgress, renderLanes); + case MemoComponent: { + const type = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + // Resolve outer props first, then resolve inner props. + let resolvedProps = resolveDefaultProps(type, unresolvedProps); + if (__DEV__) { + if (workInProgress.type !== workInProgress.elementType) { + const outerPropTypes = type.propTypes; + if (outerPropTypes) { + checkPropTypes( + outerPropTypes, + resolvedProps, // Resolved for outer only + 'prop', + getComponentNameFromType(type), + ); + } + } + } + resolvedProps = resolveDefaultProps(type.type, resolvedProps); + return updateMemoComponent( + current, + workInProgress, + type, + resolvedProps, + renderLanes, + ); + } + case SimpleMemoComponent: { + return updateSimpleMemoComponent( + current, + workInProgress, + workInProgress.type, + workInProgress.pendingProps, + renderLanes, + ); + } + case IncompleteClassComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; + const resolvedProps = + workInProgress.elementType === Component + ? unresolvedProps + : resolveDefaultProps(Component, unresolvedProps); + return mountIncompleteClassComponent( + current, + workInProgress, + Component, + resolvedProps, + renderLanes, + ); + } + case SuspenseListComponent: { + return updateSuspenseListComponent(current, workInProgress, renderLanes); + } + case ScopeComponent: { + if (enableScopeAPI) { + return updateScopeComponent(current, workInProgress, renderLanes); + } + break; + } + case OffscreenComponent: { + return updateOffscreenComponent(current, workInProgress, renderLanes); + } + case LegacyHiddenComponent: { + if (enableLegacyHidden) { + return updateLegacyHiddenComponent( + current, + workInProgress, + renderLanes, + ); + } + break; + } + case CacheComponent: { + if (enableCache) { + return updateCacheComponent(current, workInProgress, renderLanes); + } + break; + } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + return updateTracingMarkerComponent( + current, + workInProgress, + renderLanes, + ); + } + break; + } + } + + throw new Error( + `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` + + 'React. Please file an issue.', + ); +} + +export {beginWork}; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 11bd73b607a27..b289ad1725eab 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -153,7 +153,6 @@ import { clearSingleton, acquireSingletonInstance, releaseSingletonInstance, - scheduleMicrotask, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, @@ -2416,26 +2415,16 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { ); } - if ((instance._visibility & OffscreenDetached) !== NoFlags) { + if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) { // The instance is already detached, this is a noop. return; } - instance._pendingVisibility |= OffscreenDetached; - - // Detaching needs to be postoned in case attach is called before next update. - scheduleMicrotask(() => { - if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { - // Attach was called. Offscreen does not need to be detached. - return; - } - - instance._visibility |= OffscreenDetached; - const root = enqueueConcurrentRenderForLane(fiber, SyncLane); - if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); - } - }); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + instance._pendingVisibility |= OffscreenDetached; + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } export function attachOffscreenInstance(instance: OffscreenInstance): void { @@ -2446,16 +2435,14 @@ export function attachOffscreenInstance(instance: OffscreenInstance): void { ); } - instance._pendingVisibility &= ~OffscreenDetached; - - if ((instance._visibility & OffscreenDetached) === NoFlags) { + if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { // The instance is already attached, this is a noop. return; } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - instance._visibility &= ~OffscreenDetached; + instance._pendingVisibility &= ~OffscreenDetached; scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index c879678d27e57..889f8449066ed 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -153,7 +153,6 @@ import { clearSingleton, acquireSingletonInstance, releaseSingletonInstance, - scheduleMicrotask, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, @@ -2416,26 +2415,16 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { ); } - if ((instance._visibility & OffscreenDetached) !== NoFlags) { + if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) { // The instance is already detached, this is a noop. return; } - instance._pendingVisibility |= OffscreenDetached; - - // Detaching needs to be postoned in case attach is called before next update. - scheduleMicrotask(() => { - if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { - // Attach was called. Offscreen does not need to be detached. - return; - } - - instance._visibility |= OffscreenDetached; - const root = enqueueConcurrentRenderForLane(fiber, SyncLane); - if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); - } - }); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + instance._pendingVisibility |= OffscreenDetached; + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } export function attachOffscreenInstance(instance: OffscreenInstance): void { @@ -2446,16 +2435,14 @@ export function attachOffscreenInstance(instance: OffscreenInstance): void { ); } - instance._pendingVisibility &= ~OffscreenDetached; - - if ((instance._visibility & OffscreenDetached) === NoFlags) { + if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { // The instance is already attached, this is a noop. return; } const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { - instance._visibility &= ~OffscreenDetached; + instance._pendingVisibility &= ~OffscreenDetached; scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); } } diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index c44f261337fcc..d723c44a5d308 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -1663,8 +1663,8 @@ describe('ReactOffscreen', () => { nextRenderTriggerDetach = true; - // Offscreen is attached. State updates from offscreen are **not defered**. - // Offscreen is detached inside useLayoutEffect; + // Offscreen is attached and gets detached inside useLayoutEffect. + // State updates from offscreen are **defered**. await act(async () => { updateChildState(1); updateHighPriorityComponentState(1); @@ -1672,34 +1672,20 @@ describe('ReactOffscreen', () => { 'HighPriorityComponent 1', 'Child 1', 'HighPriorityComponent 2', - 'Child 2', ]); expect(root).toMatchRenderedOutput( <> - - , - ); - }); - - // Offscreen is detached. State updates from offscreen are **defered**. - await act(async () => { - updateChildState(3); - updateHighPriorityComponentState(3); - expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 3']); - expect(root).toMatchRenderedOutput( - <> - - + , ); }); - expect(Scheduler).toHaveYielded(['Child 3']); + expect(Scheduler).toHaveYielded(['Child 2']); expect(root).toMatchRenderedOutput( <> - - + + , ); @@ -1708,16 +1694,16 @@ describe('ReactOffscreen', () => { // Offscreen is detached. State updates from offscreen are **defered**. // Offscreen is attached inside useLayoutEffect; await act(async () => { - updateChildState(4); - updateHighPriorityComponentState(4); + updateChildState(3); + updateHighPriorityComponentState(3); expect(Scheduler).toFlushUntilNextPaint([ - 'HighPriorityComponent 4', - 'Child 4', + 'HighPriorityComponent 3', + 'Child 3', ]); expect(root).toMatchRenderedOutput( <> - - + + , ); }); @@ -2043,7 +2029,7 @@ describe('ReactOffscreen', () => { instance.detach(); }); - expect(Scheduler).toHaveYielded(['attach child', 'detach child']); + expect(Scheduler).toHaveYielded([]); }); // @gate enableOffscreen From 9bd4c842ac53fd8960810b1670eb19edc38f91b1 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 2 Dec 2022 17:20:45 +0000 Subject: [PATCH 12/17] Use _pendingVisibility to batch Offscreen visibility updates --- packages/react-reconciler/src/ReactFiberBeginWork.js | 10 ++-------- packages/react-reconciler/src/ReactFiberCommitWork.js | 6 ++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 38bec8b696e36..65dbb3e9e0282 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -677,24 +677,18 @@ function updateOffscreenComponent( ) { const nextProps: OffscreenProps = workInProgress.pendingProps; const nextChildren = nextProps.children; + const isPendingDetached = (workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0; const prevState: OffscreenState | null = current !== null ? current.memoizedState : null; - // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is - // to support batching of `attach` and `detach` calls. - workInProgress.stateNode._visibility &= ~OffscreenDetached; - workInProgress.stateNode._visibility |= - workInProgress.stateNode._pendingVisibility & OffscreenDetached; - markRef(current, workInProgress); if ( nextProps.mode === 'hidden' || (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding') || - // TODO: remove read from stateNode. - workInProgress.stateNode._visibility & OffscreenDetached + isPendingDetached ) { // Rendering a hidden tree. diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index b289ad1725eab..aee54674c14ca 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2879,6 +2879,12 @@ function commitMutationEffectsOnFiber( // TODO: Add explicit effect flag to set _current. finishedWork.stateNode._current = finishedWork; + // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is + // to support batching of `attach` and `detach` calls. + finishedWork.stateNode._visibility &= ~OffscreenDetached; + finishedWork.stateNode._visibility |= + finishedWork.stateNode._pendingVisibility & OffscreenDetached; + if (flags & Visibility) { const offscreenInstance: OffscreenInstance = finishedWork.stateNode; From 3d1c86f60589edbc85bffbfe72f524b1791d0c4b Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 2 Dec 2022 17:22:21 +0000 Subject: [PATCH 13/17] Remove forked files --- .../react-reconciler/src/ReactFiber.old.js | 917 ---- .../src/ReactFiberBeginWork.old.js | 4218 ---------------- .../src/ReactFiberCommitWork.old.js | 4469 ----------------- 3 files changed, 9604 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiber.old.js delete mode 100644 packages/react-reconciler/src/ReactFiberBeginWork.old.js delete mode 100644 packages/react-reconciler/src/ReactFiberCommitWork.old.js diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js deleted file mode 100644 index 3b3b9b7d259cb..0000000000000 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ /dev/null @@ -1,917 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {ReactElement} from 'shared/ReactElementType'; -import type {ReactFragment, ReactPortal, ReactScope} from 'shared/ReactTypes'; -import type {Fiber} from './ReactInternalTypes'; -import type {RootTag} from './ReactRootTags'; -import type {WorkTag} from './ReactWorkTags'; -import type {TypeOfMode} from './ReactTypeOfMode'; -import type {Lanes} from './ReactFiberLane.old'; -import type {SuspenseInstance} from './ReactFiberHostConfig'; -import type { - OffscreenProps, - OffscreenInstance, -} from './ReactFiberOffscreenComponent'; -import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old'; - -import { - supportsResources, - supportsSingletons, - isHostResourceType, - isHostSingletonType, -} from './ReactFiberHostConfig'; -import { - createRootStrictEffectsByDefault, - enableCache, - enableProfilerTimer, - enableScopeAPI, - enableLegacyHidden, - enableSyncDefaultUpdates, - allowConcurrentByDefault, - enableTransitionTracing, - enableDebugTracing, - enableFloat, - enableHostSingletons, -} from 'shared/ReactFeatureFlags'; -import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; -import {ConcurrentRoot} from './ReactRootTags'; -import { - IndeterminateComponent, - ClassComponent, - HostRoot, - HostComponent, - HostText, - HostPortal, - HostResource, - HostSingleton, - ForwardRef, - Fragment, - Mode, - ContextProvider, - ContextConsumer, - Profiler, - SuspenseComponent, - SuspenseListComponent, - DehydratedFragment, - FunctionComponent, - MemoComponent, - SimpleMemoComponent, - LazyComponent, - ScopeComponent, - OffscreenComponent, - LegacyHiddenComponent, - CacheComponent, - TracingMarkerComponent, -} from './ReactWorkTags'; -import {OffscreenVisible} from './ReactFiberOffscreenComponent'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; -import { - resolveClassForHotReloading, - resolveFunctionForHotReloading, - resolveForwardRefForHotReloading, -} from './ReactFiberHotReloading.old'; -import {NoLanes} from './ReactFiberLane.old'; -import { - NoMode, - ConcurrentMode, - DebugTracingMode, - ProfileMode, - StrictLegacyMode, - StrictEffectsMode, - ConcurrentUpdatesByDefaultMode, -} from './ReactTypeOfMode'; -import { - REACT_FORWARD_REF_TYPE, - REACT_FRAGMENT_TYPE, - REACT_DEBUG_TRACING_MODE_TYPE, - REACT_STRICT_MODE_TYPE, - REACT_PROFILER_TYPE, - REACT_PROVIDER_TYPE, - REACT_CONTEXT_TYPE, - REACT_SUSPENSE_TYPE, - REACT_SUSPENSE_LIST_TYPE, - REACT_MEMO_TYPE, - REACT_LAZY_TYPE, - REACT_SCOPE_TYPE, - REACT_OFFSCREEN_TYPE, - REACT_LEGACY_HIDDEN_TYPE, - REACT_CACHE_TYPE, - REACT_TRACING_MARKER_TYPE, -} from 'shared/ReactSymbols'; -import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old'; -import { - detachOffscreenInstance, - attachOffscreenInstance, -} from './ReactFiberCommitWork.old'; -import {getHostContext} from './ReactFiberHostContext.old'; - -export type {Fiber}; - -let hasBadMapPolyfill; - -if (__DEV__) { - hasBadMapPolyfill = false; - try { - const nonExtensibleObject = Object.preventExtensions({}); - /* eslint-disable no-new */ - new Map([[nonExtensibleObject, null]]); - new Set([nonExtensibleObject]); - /* eslint-enable no-new */ - } catch (e) { - // TODO: Consider warning about bad polyfills - hasBadMapPolyfill = true; - } -} - -function FiberNode( - tag: WorkTag, - pendingProps: mixed, - key: null | string, - mode: TypeOfMode, -) { - // Instance - this.tag = tag; - this.key = key; - this.elementType = null; - this.type = null; - this.stateNode = null; - - // Fiber - this.return = null; - this.child = null; - this.sibling = null; - this.index = 0; - - this.ref = null; - this.refCleanup = null; - - this.pendingProps = pendingProps; - this.memoizedProps = null; - this.updateQueue = null; - this.memoizedState = null; - this.dependencies = null; - - this.mode = mode; - - // Effects - this.flags = NoFlags; - this.subtreeFlags = NoFlags; - this.deletions = null; - - this.lanes = NoLanes; - this.childLanes = NoLanes; - - this.alternate = null; - - if (enableProfilerTimer) { - // Note: The following is done to avoid a v8 performance cliff. - // - // Initializing the fields below to smis and later updating them with - // double values will cause Fibers to end up having separate shapes. - // This behavior/bug has something to do with Object.preventExtension(). - // Fortunately this only impacts DEV builds. - // Unfortunately it makes React unusably slow for some applications. - // To work around this, initialize the fields below with doubles. - // - // Learn more about this here: - // https://github.com/facebook/react/issues/14365 - // https://bugs.chromium.org/p/v8/issues/detail?id=8538 - this.actualDuration = Number.NaN; - this.actualStartTime = Number.NaN; - this.selfBaseDuration = Number.NaN; - this.treeBaseDuration = Number.NaN; - - // It's okay to replace the initial doubles with smis after initialization. - // This won't trigger the performance cliff mentioned above, - // and it simplifies other profiler code (including DevTools). - this.actualDuration = 0; - this.actualStartTime = -1; - this.selfBaseDuration = 0; - this.treeBaseDuration = 0; - } - - if (__DEV__) { - // This isn't directly used but is handy for debugging internals: - - this._debugSource = null; - this._debugOwner = null; - this._debugNeedsRemount = false; - this._debugHookTypes = null; - if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') { - Object.preventExtensions(this); - } - } -} - -// This is a constructor function, rather than a POJO constructor, still -// please ensure we do the following: -// 1) Nobody should add any instance methods on this. Instance methods can be -// more difficult to predict when they get optimized and they are almost -// never inlined properly in static compilers. -// 2) Nobody should rely on `instanceof Fiber` for type testing. We should -// always know when it is a fiber. -// 3) We might want to experiment with using numeric keys since they are easier -// to optimize in a non-JIT environment. -// 4) We can easily go from a constructor to a createFiber object literal if that -// is faster. -// 5) It should be easy to port this to a C struct and keep a C implementation -// compatible. -const createFiber = function( - tag: WorkTag, - pendingProps: mixed, - key: null | string, - mode: TypeOfMode, -): Fiber { - // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors - return new FiberNode(tag, pendingProps, key, mode); -}; - -function shouldConstruct(Component: Function) { - const prototype = Component.prototype; - return !!(prototype && prototype.isReactComponent); -} - -export function isSimpleFunctionComponent(type: any): boolean { - return ( - typeof type === 'function' && - !shouldConstruct(type) && - type.defaultProps === undefined - ); -} - -export function resolveLazyComponentTag(Component: Function): WorkTag { - if (typeof Component === 'function') { - return shouldConstruct(Component) ? ClassComponent : FunctionComponent; - } else if (Component !== undefined && Component !== null) { - const $$typeof = Component.$$typeof; - if ($$typeof === REACT_FORWARD_REF_TYPE) { - return ForwardRef; - } - if ($$typeof === REACT_MEMO_TYPE) { - return MemoComponent; - } - } - return IndeterminateComponent; -} - -// This is used to create an alternate fiber to do work on. -export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { - let workInProgress = current.alternate; - if (workInProgress === null) { - // We use a double buffering pooling technique because we know that we'll - // only ever need at most two versions of a tree. We pool the "other" unused - // node that we're free to reuse. This is lazily created to avoid allocating - // extra objects for things that are never updated. It also allow us to - // reclaim the extra memory if needed. - workInProgress = createFiber( - current.tag, - pendingProps, - current.key, - current.mode, - ); - workInProgress.elementType = current.elementType; - workInProgress.type = current.type; - workInProgress.stateNode = current.stateNode; - - if (__DEV__) { - // DEV-only fields - - workInProgress._debugSource = current._debugSource; - workInProgress._debugOwner = current._debugOwner; - workInProgress._debugHookTypes = current._debugHookTypes; - } - - workInProgress.alternate = current; - current.alternate = workInProgress; - } else { - workInProgress.pendingProps = pendingProps; - // Needed because Blocks store data on type. - workInProgress.type = current.type; - - // We already have an alternate. - // Reset the effect tag. - workInProgress.flags = NoFlags; - - // The effects are no longer valid. - workInProgress.subtreeFlags = NoFlags; - workInProgress.deletions = null; - - if (enableProfilerTimer) { - // We intentionally reset, rather than copy, actualDuration & actualStartTime. - // This prevents time from endlessly accumulating in new commits. - // This has the downside of resetting values for different priority renders, - // But works for yielding (the common case) and should support resuming. - workInProgress.actualDuration = 0; - workInProgress.actualStartTime = -1; - } - } - - // Reset all effects except static ones. - // Static effects are not specific to a render. - workInProgress.flags = current.flags & StaticMask; - workInProgress.childLanes = current.childLanes; - workInProgress.lanes = current.lanes; - - workInProgress.child = current.child; - workInProgress.memoizedProps = current.memoizedProps; - workInProgress.memoizedState = current.memoizedState; - workInProgress.updateQueue = current.updateQueue; - - // Clone the dependencies object. This is mutated during the render phase, so - // it cannot be shared with the current fiber. - const currentDependencies = current.dependencies; - workInProgress.dependencies = - currentDependencies === null - ? null - : { - lanes: currentDependencies.lanes, - firstContext: currentDependencies.firstContext, - }; - - // These will be overridden during the parent's reconciliation - workInProgress.sibling = current.sibling; - workInProgress.index = current.index; - workInProgress.ref = current.ref; - workInProgress.refCleanup = current.refCleanup; - - if (enableProfilerTimer) { - workInProgress.selfBaseDuration = current.selfBaseDuration; - workInProgress.treeBaseDuration = current.treeBaseDuration; - } - - if (__DEV__) { - workInProgress._debugNeedsRemount = current._debugNeedsRemount; - switch (workInProgress.tag) { - case IndeterminateComponent: - case FunctionComponent: - case SimpleMemoComponent: - workInProgress.type = resolveFunctionForHotReloading(current.type); - break; - case ClassComponent: - workInProgress.type = resolveClassForHotReloading(current.type); - break; - case ForwardRef: - workInProgress.type = resolveForwardRefForHotReloading(current.type); - break; - default: - break; - } - } - - return workInProgress; -} - -// Used to reuse a Fiber for a second pass. -export function resetWorkInProgress( - workInProgress: Fiber, - renderLanes: Lanes, -): Fiber { - // This resets the Fiber to what createFiber or createWorkInProgress would - // have set the values to before during the first pass. Ideally this wouldn't - // be necessary but unfortunately many code paths reads from the workInProgress - // when they should be reading from current and writing to workInProgress. - - // We assume pendingProps, index, key, ref, return are still untouched to - // avoid doing another reconciliation. - - // Reset the effect flags but keep any Placement tags, since that's something - // that child fiber is setting, not the reconciliation. - workInProgress.flags &= StaticMask | Placement; - - // The effects are no longer valid. - - const current = workInProgress.alternate; - if (current === null) { - // Reset to createFiber's initial values. - workInProgress.childLanes = NoLanes; - workInProgress.lanes = renderLanes; - - workInProgress.child = null; - workInProgress.subtreeFlags = NoFlags; - workInProgress.memoizedProps = null; - workInProgress.memoizedState = null; - workInProgress.updateQueue = null; - - workInProgress.dependencies = null; - - workInProgress.stateNode = null; - - if (enableProfilerTimer) { - // Note: We don't reset the actualTime counts. It's useful to accumulate - // actual time across multiple render passes. - workInProgress.selfBaseDuration = 0; - workInProgress.treeBaseDuration = 0; - } - } else { - // Reset to the cloned values that createWorkInProgress would've. - workInProgress.childLanes = current.childLanes; - workInProgress.lanes = current.lanes; - - workInProgress.child = current.child; - workInProgress.subtreeFlags = NoFlags; - workInProgress.deletions = null; - workInProgress.memoizedProps = current.memoizedProps; - workInProgress.memoizedState = current.memoizedState; - workInProgress.updateQueue = current.updateQueue; - // Needed because Blocks store data on type. - workInProgress.type = current.type; - - // Clone the dependencies object. This is mutated during the render phase, so - // it cannot be shared with the current fiber. - const currentDependencies = current.dependencies; - workInProgress.dependencies = - currentDependencies === null - ? null - : { - lanes: currentDependencies.lanes, - firstContext: currentDependencies.firstContext, - }; - - if (enableProfilerTimer) { - // Note: We don't reset the actualTime counts. It's useful to accumulate - // actual time across multiple render passes. - workInProgress.selfBaseDuration = current.selfBaseDuration; - workInProgress.treeBaseDuration = current.treeBaseDuration; - } - } - - return workInProgress; -} - -export function createHostRootFiber( - tag: RootTag, - isStrictMode: boolean, - concurrentUpdatesByDefaultOverride: null | boolean, -): Fiber { - let mode; - if (tag === ConcurrentRoot) { - mode = ConcurrentMode; - if (isStrictMode === true || createRootStrictEffectsByDefault) { - mode |= StrictLegacyMode | StrictEffectsMode; - } - if ( - // We only use this flag for our repo tests to check both behaviors. - // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting" - !enableSyncDefaultUpdates || - // Only for internal experiments. - (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride) - ) { - mode |= ConcurrentUpdatesByDefaultMode; - } - } else { - mode = NoMode; - } - - if (enableProfilerTimer && isDevToolsPresent) { - // Always collect profile timings when DevTools are present. - // This enables DevTools to start capturing timing at any point– - // Without some nodes in the tree having empty base times. - mode |= ProfileMode; - } - - return createFiber(HostRoot, null, null, mode); -} - -export function createFiberFromTypeAndProps( - type: any, // React$ElementType - key: null | string, - pendingProps: any, - owner: null | Fiber, - mode: TypeOfMode, - lanes: Lanes, -): Fiber { - let fiberTag = IndeterminateComponent; - // The resolved type is set if we know what the final type will be. I.e. it's not lazy. - let resolvedType = type; - if (typeof type === 'function') { - if (shouldConstruct(type)) { - fiberTag = ClassComponent; - if (__DEV__) { - resolvedType = resolveClassForHotReloading(resolvedType); - } - } else { - if (__DEV__) { - resolvedType = resolveFunctionForHotReloading(resolvedType); - } - } - } else if (typeof type === 'string') { - if ( - enableFloat && - supportsResources && - enableHostSingletons && - supportsSingletons - ) { - const hostContext = getHostContext(); - fiberTag = isHostResourceType(type, pendingProps, hostContext) - ? HostResource - : isHostSingletonType(type) - ? HostSingleton - : HostComponent; - } else if (enableFloat && supportsResources) { - const hostContext = getHostContext(); - fiberTag = isHostResourceType(type, pendingProps, hostContext) - ? HostResource - : HostComponent; - } else if (enableHostSingletons && supportsSingletons) { - fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent; - } else { - fiberTag = HostComponent; - } - } else { - getTag: switch (type) { - case REACT_FRAGMENT_TYPE: - return createFiberFromFragment(pendingProps.children, mode, lanes, key); - case REACT_STRICT_MODE_TYPE: - fiberTag = Mode; - mode |= StrictLegacyMode; - if ((mode & ConcurrentMode) !== NoMode) { - // Strict effects should never run on legacy roots - mode |= StrictEffectsMode; - } - break; - case REACT_PROFILER_TYPE: - return createFiberFromProfiler(pendingProps, mode, lanes, key); - case REACT_SUSPENSE_TYPE: - return createFiberFromSuspense(pendingProps, mode, lanes, key); - case REACT_SUSPENSE_LIST_TYPE: - return createFiberFromSuspenseList(pendingProps, mode, lanes, key); - case REACT_OFFSCREEN_TYPE: - return createFiberFromOffscreen(pendingProps, mode, lanes, key); - case REACT_LEGACY_HIDDEN_TYPE: - if (enableLegacyHidden) { - return createFiberFromLegacyHidden(pendingProps, mode, lanes, key); - } - // eslint-disable-next-line no-fallthrough - case REACT_SCOPE_TYPE: - if (enableScopeAPI) { - return createFiberFromScope(type, pendingProps, mode, lanes, key); - } - // eslint-disable-next-line no-fallthrough - case REACT_CACHE_TYPE: - if (enableCache) { - return createFiberFromCache(pendingProps, mode, lanes, key); - } - // eslint-disable-next-line no-fallthrough - case REACT_TRACING_MARKER_TYPE: - if (enableTransitionTracing) { - return createFiberFromTracingMarker(pendingProps, mode, lanes, key); - } - // eslint-disable-next-line no-fallthrough - case REACT_DEBUG_TRACING_MODE_TYPE: - if (enableDebugTracing) { - fiberTag = Mode; - mode |= DebugTracingMode; - break; - } - // eslint-disable-next-line no-fallthrough - default: { - if (typeof type === 'object' && type !== null) { - switch (type.$$typeof) { - case REACT_PROVIDER_TYPE: - fiberTag = ContextProvider; - break getTag; - case REACT_CONTEXT_TYPE: - // This is a consumer - fiberTag = ContextConsumer; - break getTag; - case REACT_FORWARD_REF_TYPE: - fiberTag = ForwardRef; - if (__DEV__) { - resolvedType = resolveForwardRefForHotReloading(resolvedType); - } - break getTag; - case REACT_MEMO_TYPE: - fiberTag = MemoComponent; - break getTag; - case REACT_LAZY_TYPE: - fiberTag = LazyComponent; - resolvedType = null; - break getTag; - } - } - let info = ''; - if (__DEV__) { - if ( - type === undefined || - (typeof type === 'object' && - type !== null && - Object.keys(type).length === 0) - ) { - info += - ' You likely forgot to export your component from the file ' + - "it's defined in, or you might have mixed up default and " + - 'named imports.'; - } - const ownerName = owner ? getComponentNameFromFiber(owner) : null; - if (ownerName) { - info += '\n\nCheck the render method of `' + ownerName + '`.'; - } - } - - throw new Error( - 'Element type is invalid: expected a string (for built-in ' + - 'components) or a class/function (for composite components) ' + - `but got: ${type == null ? type : typeof type}.${info}`, - ); - } - } - } - - const fiber = createFiber(fiberTag, pendingProps, key, mode); - fiber.elementType = type; - fiber.type = resolvedType; - fiber.lanes = lanes; - - if (__DEV__) { - fiber._debugOwner = owner; - } - - return fiber; -} - -export function createFiberFromElement( - element: ReactElement, - mode: TypeOfMode, - lanes: Lanes, -): Fiber { - let owner = null; - if (__DEV__) { - owner = element._owner; - } - const type = element.type; - const key = element.key; - const pendingProps = element.props; - const fiber = createFiberFromTypeAndProps( - type, - key, - pendingProps, - owner, - mode, - lanes, - ); - if (__DEV__) { - fiber._debugSource = element._source; - fiber._debugOwner = element._owner; - } - return fiber; -} - -export function createFiberFromFragment( - elements: ReactFragment, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(Fragment, elements, key, mode); - fiber.lanes = lanes; - return fiber; -} - -function createFiberFromScope( - scope: ReactScope, - pendingProps: any, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -) { - const fiber = createFiber(ScopeComponent, pendingProps, key, mode); - fiber.type = scope; - fiber.elementType = scope; - fiber.lanes = lanes; - return fiber; -} - -function createFiberFromProfiler( - pendingProps: any, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - if (__DEV__) { - if (typeof pendingProps.id !== 'string') { - console.error( - 'Profiler must specify an "id" of type `string` as a prop. Received the type `%s` instead.', - typeof pendingProps.id, - ); - } - } - - const fiber = createFiber(Profiler, pendingProps, key, mode | ProfileMode); - fiber.elementType = REACT_PROFILER_TYPE; - fiber.lanes = lanes; - - if (enableProfilerTimer) { - fiber.stateNode = { - effectDuration: 0, - passiveEffectDuration: 0, - }; - } - - return fiber; -} - -export function createFiberFromSuspense( - pendingProps: any, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(SuspenseComponent, pendingProps, key, mode); - fiber.elementType = REACT_SUSPENSE_TYPE; - fiber.lanes = lanes; - return fiber; -} - -export function createFiberFromSuspenseList( - pendingProps: any, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode); - fiber.elementType = REACT_SUSPENSE_LIST_TYPE; - fiber.lanes = lanes; - return fiber; -} - -export function createFiberFromOffscreen( - pendingProps: OffscreenProps, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(OffscreenComponent, pendingProps, key, mode); - fiber.elementType = REACT_OFFSCREEN_TYPE; - fiber.lanes = lanes; - const primaryChildInstance: OffscreenInstance = { - _visibility: OffscreenVisible, - _pendingVisibility: OffscreenVisible, - _pendingMarkers: null, - _retryCache: null, - _transitions: null, - _current: null, - detach: () => detachOffscreenInstance(primaryChildInstance), - attach: () => attachOffscreenInstance(primaryChildInstance), - }; - fiber.stateNode = primaryChildInstance; - return fiber; -} - -export function createFiberFromLegacyHidden( - pendingProps: OffscreenProps, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode); - fiber.elementType = REACT_LEGACY_HIDDEN_TYPE; - fiber.lanes = lanes; - // Adding a stateNode for legacy hidden because it's currently using - // the offscreen implementation, which depends on a state node - const instance: OffscreenInstance = { - _visibility: OffscreenVisible, - _pendingVisibility: OffscreenVisible, - _pendingMarkers: null, - _transitions: null, - _retryCache: null, - _current: null, - detach: () => detachOffscreenInstance(instance), - attach: () => attachOffscreenInstance(instance), - }; - fiber.stateNode = instance; - return fiber; -} - -export function createFiberFromCache( - pendingProps: any, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(CacheComponent, pendingProps, key, mode); - fiber.elementType = REACT_CACHE_TYPE; - fiber.lanes = lanes; - return fiber; -} - -export function createFiberFromTracingMarker( - pendingProps: any, - mode: TypeOfMode, - lanes: Lanes, - key: null | string, -): Fiber { - const fiber = createFiber(TracingMarkerComponent, pendingProps, key, mode); - fiber.elementType = REACT_TRACING_MARKER_TYPE; - fiber.lanes = lanes; - const tracingMarkerInstance: TracingMarkerInstance = { - tag: TransitionTracingMarker, - transitions: null, - pendingBoundaries: null, - aborts: null, - name: pendingProps.name, - }; - fiber.stateNode = tracingMarkerInstance; - return fiber; -} - -export function createFiberFromText( - content: string, - mode: TypeOfMode, - lanes: Lanes, -): Fiber { - const fiber = createFiber(HostText, content, null, mode); - fiber.lanes = lanes; - return fiber; -} - -export function createFiberFromHostInstanceForDeletion(): Fiber { - const fiber = createFiber(HostComponent, null, null, NoMode); - fiber.elementType = 'DELETED'; - return fiber; -} - -export function createFiberFromDehydratedFragment( - dehydratedNode: SuspenseInstance, -): Fiber { - const fiber = createFiber(DehydratedFragment, null, null, NoMode); - fiber.stateNode = dehydratedNode; - return fiber; -} - -export function createFiberFromPortal( - portal: ReactPortal, - mode: TypeOfMode, - lanes: Lanes, -): Fiber { - const pendingProps = portal.children !== null ? portal.children : []; - const fiber = createFiber(HostPortal, pendingProps, portal.key, mode); - fiber.lanes = lanes; - fiber.stateNode = { - containerInfo: portal.containerInfo, - pendingChildren: null, // Used by persistent updates - implementation: portal.implementation, - }; - return fiber; -} - -// Used for stashing WIP properties to replay failed work in DEV. -export function assignFiberPropertiesInDEV( - target: Fiber | null, - source: Fiber, -): Fiber { - if (target === null) { - // This Fiber's initial properties will always be overwritten. - // We only use a Fiber to ensure the same hidden class so DEV isn't slow. - target = createFiber(IndeterminateComponent, null, null, NoMode); - } - - // This is intentionally written as a list of all properties. - // We tried to use Object.assign() instead but this is called in - // the hottest path, and Object.assign() was too slow: - // https://github.com/facebook/react/issues/12502 - // This code is DEV-only so size is not a concern. - - target.tag = source.tag; - target.key = source.key; - target.elementType = source.elementType; - target.type = source.type; - target.stateNode = source.stateNode; - target.return = source.return; - target.child = source.child; - target.sibling = source.sibling; - target.index = source.index; - target.ref = source.ref; - target.refCleanup = source.refCleanup; - target.pendingProps = source.pendingProps; - target.memoizedProps = source.memoizedProps; - target.updateQueue = source.updateQueue; - target.memoizedState = source.memoizedState; - target.dependencies = source.dependencies; - target.mode = source.mode; - target.flags = source.flags; - target.subtreeFlags = source.subtreeFlags; - target.deletions = source.deletions; - target.lanes = source.lanes; - target.childLanes = source.childLanes; - target.alternate = source.alternate; - if (enableProfilerTimer) { - target.actualDuration = source.actualDuration; - target.actualStartTime = source.actualStartTime; - target.selfBaseDuration = source.selfBaseDuration; - target.treeBaseDuration = source.treeBaseDuration; - } - - target._debugSource = source._debugSource; - target._debugOwner = source._debugOwner; - target._debugNeedsRemount = source._debugNeedsRemount; - target._debugHookTypes = source._debugHookTypes; - return target; -} diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js deleted file mode 100644 index c86f1ab56bafa..0000000000000 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ /dev/null @@ -1,4218 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type { - ReactProviderType, - ReactContext, - ReactNodeList, - MutableSource, -} from 'shared/ReactTypes'; -import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; -import type {Fiber, FiberRoot} from './ReactInternalTypes'; -import type {TypeOfMode} from './ReactTypeOfMode'; -import type {Lanes, Lane} from './ReactFiberLane.old'; -import type { - SuspenseState, - SuspenseListRenderState, - SuspenseListTailMode, -} from './ReactFiberSuspenseComponent.old'; -import type {SuspenseContext} from './ReactFiberSuspenseContext.old'; -import type { - OffscreenProps, - OffscreenState, - OffscreenQueue, - OffscreenInstance, -} from './ReactFiberOffscreenComponent'; -import {OffscreenDetached} from './ReactFiberOffscreenComponent'; -import type { - Cache, - CacheComponentState, - SpawnedCachePool, -} from './ReactFiberCacheComponent.old'; -import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; -import type {RootState} from './ReactFiberRoot.old'; -import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old'; - -import checkPropTypes from 'shared/checkPropTypes'; -import { - markComponentRenderStarted, - markComponentRenderStopped, - setIsStrictModeForDevtools, -} from './ReactFiberDevToolsHook.old'; -import { - IndeterminateComponent, - FunctionComponent, - ClassComponent, - HostRoot, - HostComponent, - HostResource, - HostSingleton, - HostText, - HostPortal, - ForwardRef, - Fragment, - Mode, - ContextProvider, - ContextConsumer, - Profiler, - SuspenseComponent, - SuspenseListComponent, - MemoComponent, - SimpleMemoComponent, - LazyComponent, - IncompleteClassComponent, - ScopeComponent, - OffscreenComponent, - LegacyHiddenComponent, - CacheComponent, - TracingMarkerComponent, -} from './ReactWorkTags'; -import { - NoFlags, - PerformedWork, - Placement, - Hydrating, - ContentReset, - DidCapture, - Update, - Ref, - RefStatic, - ChildDeletion, - ForceUpdateForLegacySuspense, - StaticMask, - ShouldCapture, - ForceClientRender, - Passive, -} from './ReactFiberFlags'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import { - debugRenderPhaseSideEffectsForStrictMode, - disableLegacyContext, - disableModulePatternComponents, - enableProfilerCommitHooks, - enableProfilerTimer, - warnAboutDefaultPropsOnFunctionComponents, - enableScopeAPI, - enableCache, - enableLazyContextPropagation, - enableSchedulingProfiler, - enableTransitionTracing, - enableLegacyHidden, - enableCPUSuspense, - enableUseMutableSource, - enableFloat, - enableHostSingletons, -} from 'shared/ReactFeatureFlags'; -import isArray from 'shared/isArray'; -import shallowEqual from 'shared/shallowEqual'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import getComponentNameFromType from 'shared/getComponentNameFromType'; -import ReactStrictModeWarnings from './ReactStrictModeWarnings.old'; -import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols'; -import { - getCurrentFiberOwnerNameInDevOrNull, - setIsRendering, -} from './ReactCurrentFiber'; -import { - resolveFunctionForHotReloading, - resolveForwardRefForHotReloading, - resolveClassForHotReloading, -} from './ReactFiberHotReloading.old'; - -import { - mountChildFibers, - reconcileChildFibers, - cloneChildFibers, -} from './ReactChildFiber.old'; -import { - processUpdateQueue, - cloneUpdateQueue, - initializeUpdateQueue, - enqueueCapturedUpdate, -} from './ReactFiberClassUpdateQueue.old'; -import { - NoLane, - NoLanes, - SyncLane, - OffscreenLane, - DefaultHydrationLane, - SomeRetryLane, - NoTimestamp, - includesSomeLane, - laneToLanes, - removeLanes, - mergeLanes, - getBumpedLaneForHydration, - pickArbitraryLane, -} from './ReactFiberLane.old'; -import { - ConcurrentMode, - NoMode, - ProfileMode, - StrictLegacyMode, -} from './ReactTypeOfMode'; -import { - shouldSetTextContent, - isSuspenseInstancePending, - isSuspenseInstanceFallback, - getSuspenseInstanceFallbackErrorDetails, - registerSuspenseInstanceRetry, - supportsHydration, - supportsResources, - supportsSingletons, - isPrimaryRenderer, - getResource, -} from './ReactFiberHostConfig'; -import type {SuspenseInstance} from './ReactFiberHostConfig'; -import {shouldError, shouldSuspend} from './ReactFiberReconciler'; -import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old'; -import { - suspenseStackCursor, - pushSuspenseListContext, - ForceSuspenseFallback, - hasSuspenseListContext, - setDefaultShallowSuspenseListContext, - setShallowSuspenseListContext, - pushPrimaryTreeSuspenseHandler, - pushFallbackTreeSuspenseHandler, - pushOffscreenSuspenseHandler, - reuseSuspenseHandlerOnStack, - popSuspenseHandler, -} from './ReactFiberSuspenseContext.old'; -import { - pushHiddenContext, - reuseHiddenContextOnStack, -} from './ReactFiberHiddenContext.old'; -import {findFirstSuspended} from './ReactFiberSuspenseComponent.old'; -import { - pushProvider, - propagateContextChange, - lazilyPropagateParentContextChanges, - propagateParentContextChangesToDeferredTree, - checkIfContextChanged, - readContext, - prepareToReadContext, - scheduleContextWorkOnParentPath, -} from './ReactFiberNewContext.old'; -import { - renderWithHooks, - checkDidRenderIdHook, - bailoutHooks, - replaySuspendedComponentWithHooks, -} from './ReactFiberHooks.old'; -import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old'; -import { - getMaskedContext, - getUnmaskedContext, - hasContextChanged as hasLegacyContextChanged, - pushContextProvider as pushLegacyContextProvider, - isContextProvider as isLegacyContextProvider, - pushTopLevelContextObject, - invalidateContextProvider, -} from './ReactFiberContext.old'; -import { - getIsHydrating, - enterHydrationState, - reenterHydrationStateFromDehydratedSuspenseInstance, - resetHydrationState, - claimHydratableSingleton, - tryToClaimNextHydratableInstance, - warnIfHydrating, - queueHydrationError, -} from './ReactFiberHydrationContext.old'; -import { - adoptClassInstance, - constructClassInstance, - mountClassInstance, - resumeMountClassInstance, - updateClassInstance, -} from './ReactFiberClassComponent.old'; -import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; -import { - resolveLazyComponentTag, - createFiberFromTypeAndProps, - createFiberFromFragment, - createFiberFromOffscreen, - createWorkInProgress, - isSimpleFunctionComponent, -} from './ReactFiber.old'; -import { - retryDehydratedSuspenseBoundary, - scheduleUpdateOnFiber, - renderDidSuspendDelayIfPossible, - markSkippedUpdateLanes, - getWorkInProgressRoot, -} from './ReactFiberWorkLoop.old'; -import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; -import {setWorkInProgressVersion} from './ReactMutableSource.old'; -import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.old'; -import { - createCapturedValue, - createCapturedValueAtFiber, - type CapturedValue, -} from './ReactCapturedValue'; -import {createClassErrorUpdate} from './ReactFiberThrow.old'; -import is from 'shared/objectIs'; -import { - getForksAtLevel, - isForkedChild, - pushTreeId, - pushMaterializedTreeId, -} from './ReactFiberTreeContext.old'; -import { - requestCacheFromPool, - pushRootTransition, - getSuspendedCache, - pushTransition, - getOffscreenDeferredCache, - getPendingTransitions, -} from './ReactFiberTransition.old'; -import { - getMarkerInstances, - pushMarkerInstance, - pushRootMarkerInstance, - TransitionTracingMarker, -} from './ReactFiberTracingMarkerComponent.old'; - -const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; - -// A special exception that's used to unwind the stack when an update flows -// into a dehydrated boundary. -export const SelectiveHydrationException: mixed = new Error( - "This is not a real error. It's an implementation detail of React's " + - "selective hydration feature. If this leaks into userspace, it's a bug in " + - 'React. Please file an issue.', -); - -let didReceiveUpdate: boolean = false; - -let didWarnAboutBadClass; -let didWarnAboutModulePatternComponent; -let didWarnAboutContextTypeOnFunctionComponent; -let didWarnAboutGetDerivedStateOnFunctionComponent; -let didWarnAboutFunctionRefs; -export let didWarnAboutReassigningProps: boolean; -let didWarnAboutRevealOrder; -let didWarnAboutTailOptions; -let didWarnAboutDefaultPropsOnFunctionComponent; - -if (__DEV__) { - didWarnAboutBadClass = {}; - didWarnAboutModulePatternComponent = {}; - didWarnAboutContextTypeOnFunctionComponent = {}; - didWarnAboutGetDerivedStateOnFunctionComponent = {}; - didWarnAboutFunctionRefs = {}; - didWarnAboutReassigningProps = false; - didWarnAboutRevealOrder = {}; - didWarnAboutTailOptions = {}; - didWarnAboutDefaultPropsOnFunctionComponent = {}; -} - -export function reconcileChildren( - current: Fiber | null, - workInProgress: Fiber, - nextChildren: any, - renderLanes: Lanes, -) { - if (current === null) { - // If this is a fresh new component that hasn't been rendered yet, we - // won't update its child set by applying minimal side-effects. Instead, - // we will add them all to the child before it gets rendered. That means - // we can optimize this reconciliation pass by not tracking side-effects. - workInProgress.child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - } else { - // If the current child is the same as the work in progress, it means that - // we haven't yet started any work on these children. Therefore, we use - // the clone algorithm to create a copy of all the current children. - - // If we had any progressed work already, that is invalid at this point so - // let's throw it out. - workInProgress.child = reconcileChildFibers( - workInProgress, - current.child, - nextChildren, - renderLanes, - ); - } -} - -function forceUnmountCurrentAndReconcile( - current: Fiber, - workInProgress: Fiber, - nextChildren: any, - renderLanes: Lanes, -) { - // This function is fork of reconcileChildren. It's used in cases where we - // want to reconcile without matching against the existing set. This has the - // effect of all current children being unmounted; even if the type and key - // are the same, the old child is unmounted and a new child is created. - // - // To do this, we're going to go through the reconcile algorithm twice. In - // the first pass, we schedule a deletion for all the current children by - // passing null. - workInProgress.child = reconcileChildFibers( - workInProgress, - current.child, - null, - renderLanes, - ); - // In the second pass, we mount the new children. The trick here is that we - // pass null in place of where we usually pass the current child set. This has - // the effect of remounting all children regardless of whether their - // identities match. - workInProgress.child = reconcileChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); -} - -function updateForwardRef( - current: Fiber | null, - workInProgress: Fiber, - Component: any, - nextProps: any, - renderLanes: Lanes, -) { - // TODO: current can be non-null here even if the component - // hasn't yet mounted. This happens after the first render suspends. - // We'll need to figure out if this is fine or can cause issues. - - if (__DEV__) { - if (workInProgress.type !== workInProgress.elementType) { - // Lazy component props can't be validated in createElement - // because they're only guaranteed to be resolved here. - const innerPropTypes = Component.propTypes; - if (innerPropTypes) { - checkPropTypes( - innerPropTypes, - nextProps, // Resolved props - 'prop', - getComponentNameFromType(Component), - ); - } - } - } - - const render = Component.render; - const ref = workInProgress.ref; - - // The rest is a fork of updateFunctionComponent - let nextChildren; - let hasId; - prepareToReadContext(workInProgress, renderLanes); - if (enableSchedulingProfiler) { - markComponentRenderStarted(workInProgress); - } - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - setIsRendering(true); - nextChildren = renderWithHooks( - current, - workInProgress, - render, - nextProps, - ref, - renderLanes, - ); - hasId = checkDidRenderIdHook(); - setIsRendering(false); - } else { - nextChildren = renderWithHooks( - current, - workInProgress, - render, - nextProps, - ref, - renderLanes, - ); - hasId = checkDidRenderIdHook(); - } - if (enableSchedulingProfiler) { - markComponentRenderStopped(); - } - - if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderLanes); - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } - - if (getIsHydrating() && hasId) { - pushMaterializedTreeId(workInProgress); - } - - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateMemoComponent( - current: Fiber | null, - workInProgress: Fiber, - Component: any, - nextProps: any, - renderLanes: Lanes, -): null | Fiber { - if (current === null) { - const type = Component.type; - if ( - isSimpleFunctionComponent(type) && - Component.compare === null && - // SimpleMemoComponent codepath doesn't resolve outer props either. - Component.defaultProps === undefined - ) { - let resolvedType = type; - if (__DEV__) { - resolvedType = resolveFunctionForHotReloading(type); - } - // If this is a plain function component without default props, - // and with only the default shallow comparison, we upgrade it - // to a SimpleMemoComponent to allow fast path updates. - workInProgress.tag = SimpleMemoComponent; - workInProgress.type = resolvedType; - if (__DEV__) { - validateFunctionComponentInDev(workInProgress, type); - } - return updateSimpleMemoComponent( - current, - workInProgress, - resolvedType, - nextProps, - renderLanes, - ); - } - if (__DEV__) { - const innerPropTypes = type.propTypes; - if (innerPropTypes) { - // Inner memo component props aren't currently validated in createElement. - // We could move it there, but we'd still need this for lazy code path. - checkPropTypes( - innerPropTypes, - nextProps, // Resolved props - 'prop', - getComponentNameFromType(type), - ); - } - if ( - warnAboutDefaultPropsOnFunctionComponents && - Component.defaultProps !== undefined - ) { - const componentName = getComponentNameFromType(type) || 'Unknown'; - if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { - console.error( - '%s: Support for defaultProps will be removed from memo components ' + - 'in a future major release. Use JavaScript default parameters instead.', - componentName, - ); - didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; - } - } - } - const child = createFiberFromTypeAndProps( - Component.type, - null, - nextProps, - workInProgress, - workInProgress.mode, - renderLanes, - ); - child.ref = workInProgress.ref; - child.return = workInProgress; - workInProgress.child = child; - return child; - } - if (__DEV__) { - const type = Component.type; - const innerPropTypes = type.propTypes; - if (innerPropTypes) { - // Inner memo component props aren't currently validated in createElement. - // We could move it there, but we'd still need this for lazy code path. - checkPropTypes( - innerPropTypes, - nextProps, // Resolved props - 'prop', - getComponentNameFromType(type), - ); - } - } - const currentChild = ((current.child: any): Fiber); // This is always exactly one child - const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( - current, - renderLanes, - ); - if (!hasScheduledUpdateOrContext) { - // This will be the props with resolved defaultProps, - // unlike current.memoizedProps which will be the unresolved ones. - const prevProps = currentChild.memoizedProps; - // Default to shallow comparison - let compare = Component.compare; - compare = compare !== null ? compare : shallowEqual; - if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } - } - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - const newChild = createWorkInProgress(currentChild, nextProps); - newChild.ref = workInProgress.ref; - newChild.return = workInProgress; - workInProgress.child = newChild; - return newChild; -} - -function updateSimpleMemoComponent( - current: Fiber | null, - workInProgress: Fiber, - Component: any, - nextProps: any, - renderLanes: Lanes, -): null | Fiber { - // TODO: current can be non-null here even if the component - // hasn't yet mounted. This happens when the inner render suspends. - // We'll need to figure out if this is fine or can cause issues. - - if (__DEV__) { - if (workInProgress.type !== workInProgress.elementType) { - // Lazy component props can't be validated in createElement - // because they're only guaranteed to be resolved here. - let outerMemoType = workInProgress.elementType; - if (outerMemoType.$$typeof === REACT_LAZY_TYPE) { - // We warn when you define propTypes on lazy() - // so let's just skip over it to find memo() outer wrapper. - // Inner props for memo are validated later. - const lazyComponent: LazyComponentType = outerMemoType; - const payload = lazyComponent._payload; - const init = lazyComponent._init; - try { - outerMemoType = init(payload); - } catch (x) { - // $FlowFixMe[incompatible-type] found when upgrading Flow - outerMemoType = null; - } - // Inner propTypes will be validated in the function component path. - const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes; - if (outerPropTypes) { - checkPropTypes( - outerPropTypes, - nextProps, // Resolved (SimpleMemoComponent has no defaultProps) - 'prop', - getComponentNameFromType(outerMemoType), - ); - } - } - } - } - if (current !== null) { - const prevProps = current.memoizedProps; - if ( - shallowEqual(prevProps, nextProps) && - current.ref === workInProgress.ref && - // Prevent bailout if the implementation changed due to hot reload. - (__DEV__ ? workInProgress.type === current.type : true) - ) { - didReceiveUpdate = false; - - // The props are shallowly equal. Reuse the previous props object, like we - // would during a normal fiber bailout. - // - // We don't have strong guarantees that the props object is referentially - // equal during updates where we can't bail out anyway — like if the props - // are shallowly equal, but there's a local state or context update in the - // same batch. - // - // However, as a principle, we should aim to make the behavior consistent - // across different ways of memoizing a component. For example, React.memo - // has a different internal Fiber layout if you pass a normal function - // component (SimpleMemoComponent) versus if you pass a different type - // like forwardRef (MemoComponent). But this is an implementation detail. - // Wrapping a component in forwardRef (or React.lazy, etc) shouldn't - // affect whether the props object is reused during a bailout. - workInProgress.pendingProps = nextProps = prevProps; - - if (!checkScheduledUpdateOrContext(current, renderLanes)) { - // The pending lanes were cleared at the beginning of beginWork. We're - // about to bail out, but there might be other lanes that weren't - // included in the current render. Usually, the priority level of the - // remaining updates is accumulated during the evaluation of the - // component (i.e. when processing the update queue). But since since - // we're bailing out early *without* evaluating the component, we need - // to account for it here, too. Reset to the value of the current fiber. - // NOTE: This only applies to SimpleMemoComponent, not MemoComponent, - // because a MemoComponent fiber does not have hooks or an update queue; - // rather, it wraps around an inner component, which may or may not - // contains hooks. - // TODO: Move the reset at in beginWork out of the common path so that - // this is no longer necessary. - workInProgress.lanes = current.lanes; - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderLanes, - ); - } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { - // This is a special case that only exists for legacy mode. - // See https://github.com/facebook/react/pull/19216. - didReceiveUpdate = true; - } - } - } - return updateFunctionComponent( - current, - workInProgress, - Component, - nextProps, - renderLanes, - ); -} - -function updateOffscreenComponent( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - const nextProps: OffscreenProps = workInProgress.pendingProps; - const nextChildren = nextProps.children; - - const prevState: OffscreenState | null = - current !== null ? current.memoizedState : null; - - // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is - // to support batching of `attach` and `detach` calls. - workInProgress.stateNode._visibility &= ~OffscreenDetached; - workInProgress.stateNode._visibility |= - workInProgress.stateNode._pendingVisibility & OffscreenDetached; - - markRef(current, workInProgress); - - if ( - nextProps.mode === 'hidden' || - (enableLegacyHidden && - nextProps.mode === 'unstable-defer-without-hiding') || - // TODO: remove read from stateNode. - workInProgress.stateNode._visibility & OffscreenDetached - ) { - // Rendering a hidden tree. - - const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags; - if (didSuspend) { - // Something suspended inside a hidden tree - - // Include the base lanes from the last render - const nextBaseLanes = - prevState !== null - ? mergeLanes(prevState.baseLanes, renderLanes) - : renderLanes; - - if (current !== null) { - // Reset to the current children - let currentChild = (workInProgress.child = current.child); - - // The current render suspended, but there may be other lanes with - // pending work. We can't read `childLanes` from the current Offscreen - // fiber because we reset it when it was deferred; however, we can read - // the pending lanes from the child fibers. - let currentChildLanes = NoLanes; - while (currentChild !== null) { - currentChildLanes = mergeLanes( - mergeLanes(currentChildLanes, currentChild.lanes), - currentChild.childLanes, - ); - currentChild = currentChild.sibling; - } - const lanesWeJustAttempted = nextBaseLanes; - const remainingChildLanes = removeLanes( - currentChildLanes, - lanesWeJustAttempted, - ); - workInProgress.childLanes = remainingChildLanes; - } else { - workInProgress.childLanes = NoLanes; - workInProgress.child = null; - } - - return deferHiddenOffscreenComponent( - current, - workInProgress, - nextBaseLanes, - renderLanes, - ); - } - - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - // In legacy sync mode, don't defer the subtree. Render it now. - // TODO: Consider how Offscreen should work with transitions in the future - const nextState: OffscreenState = { - baseLanes: NoLanes, - cachePool: null, - }; - workInProgress.memoizedState = nextState; - if (enableCache) { - // push the cache pool even though we're going to bail out - // because otherwise there'd be a context mismatch - if (current !== null) { - pushTransition(workInProgress, null, null); - } - } - reuseHiddenContextOnStack(workInProgress); - pushOffscreenSuspenseHandler(workInProgress); - } else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) { - // We're hidden, and we're not rendering at Offscreen. We will bail out - // and resume this tree later. - - // Schedule this fiber to re-render at Offscreen priority - workInProgress.lanes = workInProgress.childLanes = laneToLanes( - OffscreenLane, - ); - - // Include the base lanes from the last render - const nextBaseLanes = - prevState !== null - ? mergeLanes(prevState.baseLanes, renderLanes) - : renderLanes; - - return deferHiddenOffscreenComponent( - current, - workInProgress, - nextBaseLanes, - renderLanes, - ); - } else { - // This is the second render. The surrounding visible content has already - // committed. Now we resume rendering the hidden tree. - - // Rendering at offscreen, so we can clear the base lanes. - const nextState: OffscreenState = { - baseLanes: NoLanes, - cachePool: null, - }; - workInProgress.memoizedState = nextState; - if (enableCache && current !== null) { - // If the render that spawned this one accessed the cache pool, resume - // using the same cache. Unless the parent changed, since that means - // there was a refresh. - const prevCachePool = prevState !== null ? prevState.cachePool : null; - // TODO: Consider if and how Offscreen pre-rendering should - // be attributed to the transition that spawned it - pushTransition(workInProgress, prevCachePool, null); - } - - // Push the lanes that were skipped when we bailed out. - if (prevState !== null) { - pushHiddenContext(workInProgress, prevState); - } else { - reuseHiddenContextOnStack(workInProgress); - } - pushOffscreenSuspenseHandler(workInProgress); - } - } else { - // Rendering a visible tree. - if (prevState !== null) { - // We're going from hidden -> visible. - let prevCachePool = null; - if (enableCache) { - // If the render that spawned this one accessed the cache pool, resume - // using the same cache. Unless the parent changed, since that means - // there was a refresh. - prevCachePool = prevState.cachePool; - } - - let transitions = null; - if (enableTransitionTracing) { - // We have now gone from hidden to visible, so any transitions should - // be added to the stack to get added to any Offscreen/suspense children - const instance: OffscreenInstance | null = workInProgress.stateNode; - if (instance !== null && instance._transitions != null) { - transitions = Array.from(instance._transitions); - } - } - - pushTransition(workInProgress, prevCachePool, transitions); - - // Push the lanes that were skipped when we bailed out. - pushHiddenContext(workInProgress, prevState); - reuseSuspenseHandlerOnStack(workInProgress); - - // Since we're not hidden anymore, reset the state - workInProgress.memoizedState = null; - } else { - // We weren't previously hidden, and we still aren't, so there's nothing - // special to do. Need to push to the stack regardless, though, to avoid - // a push/pop misalignment. - - if (enableCache) { - // If the render that spawned this one accessed the cache pool, resume - // using the same cache. Unless the parent changed, since that means - // there was a refresh. - if (current !== null) { - pushTransition(workInProgress, null, null); - } - } - - // We're about to bail out, but we need to push this to the stack anyway - // to avoid a push/pop misalignment. - reuseHiddenContextOnStack(workInProgress); - reuseSuspenseHandlerOnStack(workInProgress); - } - } - - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function deferHiddenOffscreenComponent( - current: Fiber | null, - workInProgress: Fiber, - nextBaseLanes: Lanes, - renderLanes: Lanes, -) { - const nextState: OffscreenState = { - baseLanes: nextBaseLanes, - // Save the cache pool so we can resume later. - cachePool: enableCache ? getOffscreenDeferredCache() : null, - }; - workInProgress.memoizedState = nextState; - if (enableCache) { - // push the cache pool even though we're going to bail out - // because otherwise there'd be a context mismatch - if (current !== null) { - pushTransition(workInProgress, null, null); - } - } - - // We're about to bail out, but we need to push this to the stack anyway - // to avoid a push/pop misalignment. - reuseHiddenContextOnStack(workInProgress); - - pushOffscreenSuspenseHandler(workInProgress); - - if (enableLazyContextPropagation && current !== null) { - // Since this tree will resume rendering in a separate render, we need - // to propagate parent contexts now so we don't lose track of which - // ones changed. - propagateParentContextChangesToDeferredTree( - current, - workInProgress, - renderLanes, - ); - } - - return null; -} - -// Note: These happen to have identical begin phases, for now. We shouldn't hold -// ourselves to this constraint, though. If the behavior diverges, we should -// fork the function. -const updateLegacyHiddenComponent = updateOffscreenComponent; - -function updateCacheComponent( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - if (!enableCache) { - return null; - } - - prepareToReadContext(workInProgress, renderLanes); - const parentCache = readContext(CacheContext); - - if (current === null) { - // Initial mount. Request a fresh cache from the pool. - const freshCache = requestCacheFromPool(renderLanes); - const initialState: CacheComponentState = { - parent: parentCache, - cache: freshCache, - }; - workInProgress.memoizedState = initialState; - initializeUpdateQueue(workInProgress); - pushCacheProvider(workInProgress, freshCache); - } else { - // Check for updates - if (includesSomeLane(current.lanes, renderLanes)) { - cloneUpdateQueue(current, workInProgress); - processUpdateQueue(workInProgress, null, null, renderLanes); - } - const prevState: CacheComponentState = current.memoizedState; - const nextState: CacheComponentState = workInProgress.memoizedState; - - // Compare the new parent cache to the previous to see detect there was - // a refresh. - if (prevState.parent !== parentCache) { - // Refresh in parent. Update the parent. - const derivedState: CacheComponentState = { - parent: parentCache, - cache: parentCache, - }; - - // Copied from getDerivedStateFromProps implementation. Once the update - // queue is empty, persist the derived state onto the base state. - workInProgress.memoizedState = derivedState; - if (workInProgress.lanes === NoLanes) { - const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); - workInProgress.memoizedState = updateQueue.baseState = derivedState; - } - - pushCacheProvider(workInProgress, parentCache); - // No need to propagate a context change because the refreshed parent - // already did. - } else { - // The parent didn't refresh. Now check if this cache did. - const nextCache = nextState.cache; - pushCacheProvider(workInProgress, nextCache); - if (nextCache !== prevState.cache) { - // This cache refreshed. Propagate a context change. - propagateContextChange(workInProgress, CacheContext, renderLanes); - } - } - } - - const nextChildren = workInProgress.pendingProps.children; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -// This should only be called if the name changes -function updateTracingMarkerComponent( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - if (!enableTransitionTracing) { - return null; - } - - // TODO: (luna) Only update the tracing marker if it's newly rendered or it's name changed. - // A tracing marker is only associated with the transitions that rendered - // or updated it, so we can create a new set of transitions each time - if (current === null) { - const currentTransitions = getPendingTransitions(); - if (currentTransitions !== null) { - const markerInstance: TracingMarkerInstance = { - tag: TransitionTracingMarker, - transitions: new Set(currentTransitions), - pendingBoundaries: null, - name: workInProgress.pendingProps.name, - aborts: null, - }; - workInProgress.stateNode = markerInstance; - - // We call the marker complete callback when all child suspense boundaries resolve. - // We do this in the commit phase on Offscreen. If the marker has no child suspense - // boundaries, we need to schedule a passive effect to make sure we call the marker - // complete callback. - workInProgress.flags |= Passive; - } - } else { - if (__DEV__) { - if (current.memoizedProps.name !== workInProgress.pendingProps.name) { - console.error( - 'Changing the name of a tracing marker after mount is not supported. ' + - 'To remount the tracing marker, pass it a new key.', - ); - } - } - } - - const instance: TracingMarkerInstance | null = workInProgress.stateNode; - if (instance !== null) { - pushMarkerInstance(workInProgress, instance); - } - const nextChildren = workInProgress.pendingProps.children; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateFragment( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - const nextChildren = workInProgress.pendingProps; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateMode( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - const nextChildren = workInProgress.pendingProps.children; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateProfiler( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - if (enableProfilerTimer) { - workInProgress.flags |= Update; - - if (enableProfilerCommitHooks) { - // Reset effect durations for the next eventual effect phase. - // These are reset during render to allow the DevTools commit hook a chance to read them, - const stateNode = workInProgress.stateNode; - stateNode.effectDuration = 0; - stateNode.passiveEffectDuration = 0; - } - } - const nextProps = workInProgress.pendingProps; - const nextChildren = nextProps.children; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function markRef(current: Fiber | null, workInProgress: Fiber) { - const ref = workInProgress.ref; - if ( - (current === null && ref !== null) || - (current !== null && current.ref !== ref) - ) { - // Schedule a Ref effect - workInProgress.flags |= Ref; - workInProgress.flags |= RefStatic; - } -} - -function updateFunctionComponent( - current, - workInProgress, - Component, - nextProps: any, - renderLanes, -) { - if (__DEV__) { - if (workInProgress.type !== workInProgress.elementType) { - // Lazy component props can't be validated in createElement - // because they're only guaranteed to be resolved here. - const innerPropTypes = Component.propTypes; - if (innerPropTypes) { - checkPropTypes( - innerPropTypes, - nextProps, // Resolved props - 'prop', - getComponentNameFromType(Component), - ); - } - } - } - - let context; - if (!disableLegacyContext) { - const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); - context = getMaskedContext(workInProgress, unmaskedContext); - } - - let nextChildren; - let hasId; - prepareToReadContext(workInProgress, renderLanes); - if (enableSchedulingProfiler) { - markComponentRenderStarted(workInProgress); - } - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - setIsRendering(true); - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderLanes, - ); - hasId = checkDidRenderIdHook(); - setIsRendering(false); - } else { - nextChildren = renderWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - renderLanes, - ); - hasId = checkDidRenderIdHook(); - } - if (enableSchedulingProfiler) { - markComponentRenderStopped(); - } - - if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderLanes); - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } - - if (getIsHydrating() && hasId) { - pushMaterializedTreeId(workInProgress); - } - - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -export function replayFunctionComponent( - current: Fiber | null, - workInProgress: Fiber, - nextProps: any, - Component: any, - renderLanes: Lanes, -): Fiber | null { - // This function is used to replay a component that previously suspended, - // after its data resolves. It's a simplified version of - // updateFunctionComponent that reuses the hooks from the previous attempt. - - let context; - if (!disableLegacyContext) { - const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); - context = getMaskedContext(workInProgress, unmaskedContext); - } - - prepareToReadContext(workInProgress, renderLanes); - if (enableSchedulingProfiler) { - markComponentRenderStarted(workInProgress); - } - const nextChildren = replaySuspendedComponentWithHooks( - current, - workInProgress, - Component, - nextProps, - context, - ); - const hasId = checkDidRenderIdHook(); - if (enableSchedulingProfiler) { - markComponentRenderStopped(); - } - - if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderLanes); - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } - - if (getIsHydrating() && hasId) { - pushMaterializedTreeId(workInProgress); - } - - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateClassComponent( - current: Fiber | null, - workInProgress: Fiber, - Component: any, - nextProps: any, - renderLanes: Lanes, -) { - if (__DEV__) { - // This is used by DevTools to force a boundary to error. - switch (shouldError(workInProgress)) { - case false: { - const instance = workInProgress.stateNode; - const ctor = workInProgress.type; - // TODO This way of resetting the error boundary state is a hack. - // Is there a better way to do this? - const tempInstance = new ctor( - workInProgress.memoizedProps, - instance.context, - ); - const state = tempInstance.state; - instance.updater.enqueueSetState(instance, state, null); - break; - } - case true: { - workInProgress.flags |= DidCapture; - workInProgress.flags |= ShouldCapture; - // eslint-disable-next-line react-internal/prod-error-codes - const error = new Error('Simulated error coming from DevTools'); - const lane = pickArbitraryLane(renderLanes); - workInProgress.lanes = mergeLanes(workInProgress.lanes, lane); - // Schedule the error boundary to re-render using updated state - const update = createClassErrorUpdate( - workInProgress, - createCapturedValueAtFiber(error, workInProgress), - lane, - ); - enqueueCapturedUpdate(workInProgress, update); - break; - } - } - - if (workInProgress.type !== workInProgress.elementType) { - // Lazy component props can't be validated in createElement - // because they're only guaranteed to be resolved here. - const innerPropTypes = Component.propTypes; - if (innerPropTypes) { - checkPropTypes( - innerPropTypes, - nextProps, // Resolved props - 'prop', - getComponentNameFromType(Component), - ); - } - } - } - - // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - let hasContext; - if (isLegacyContextProvider(Component)) { - hasContext = true; - pushLegacyContextProvider(workInProgress); - } else { - hasContext = false; - } - prepareToReadContext(workInProgress, renderLanes); - - const instance = workInProgress.stateNode; - let shouldUpdate; - if (instance === null) { - resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress); - - // In the initial pass we might need to construct the instance. - constructClassInstance(workInProgress, Component, nextProps); - mountClassInstance(workInProgress, Component, nextProps, renderLanes); - shouldUpdate = true; - } else if (current === null) { - // In a resume, we'll already have an instance we can reuse. - shouldUpdate = resumeMountClassInstance( - workInProgress, - Component, - nextProps, - renderLanes, - ); - } else { - shouldUpdate = updateClassInstance( - current, - workInProgress, - Component, - nextProps, - renderLanes, - ); - } - const nextUnitOfWork = finishClassComponent( - current, - workInProgress, - Component, - shouldUpdate, - hasContext, - renderLanes, - ); - if (__DEV__) { - const inst = workInProgress.stateNode; - if (shouldUpdate && inst.props !== nextProps) { - if (!didWarnAboutReassigningProps) { - console.error( - 'It looks like %s is reassigning its own `this.props` while rendering. ' + - 'This is not supported and can lead to confusing bugs.', - getComponentNameFromFiber(workInProgress) || 'a component', - ); - } - didWarnAboutReassigningProps = true; - } - } - return nextUnitOfWork; -} - -function finishClassComponent( - current: Fiber | null, - workInProgress: Fiber, - Component: any, - shouldUpdate: boolean, - hasContext: boolean, - renderLanes: Lanes, -) { - // Refs should update even if shouldComponentUpdate returns false - markRef(current, workInProgress); - - const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags; - - if (!shouldUpdate && !didCaptureError) { - // Context providers should defer to sCU for rendering - if (hasContext) { - invalidateContextProvider(workInProgress, Component, false); - } - - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } - - const instance = workInProgress.stateNode; - - // Rerender - ReactCurrentOwner.current = workInProgress; - let nextChildren; - if ( - didCaptureError && - typeof Component.getDerivedStateFromError !== 'function' - ) { - // If we captured an error, but getDerivedStateFromError is not defined, - // unmount all the children. componentDidCatch will schedule an update to - // re-render a fallback. This is temporary until we migrate everyone to - // the new API. - // TODO: Warn in a future release. - nextChildren = null; - - if (enableProfilerTimer) { - stopProfilerTimerIfRunning(workInProgress); - } - } else { - if (enableSchedulingProfiler) { - markComponentRenderStarted(workInProgress); - } - if (__DEV__) { - setIsRendering(true); - nextChildren = instance.render(); - if ( - debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictLegacyMode - ) { - setIsStrictModeForDevtools(true); - try { - instance.render(); - } finally { - setIsStrictModeForDevtools(false); - } - } - setIsRendering(false); - } else { - nextChildren = instance.render(); - } - if (enableSchedulingProfiler) { - markComponentRenderStopped(); - } - } - - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - if (current !== null && didCaptureError) { - // If we're recovering from an error, reconcile without reusing any of - // the existing children. Conceptually, the normal children and the children - // that are shown on error are two different sets, so we shouldn't reuse - // normal children even if their identities match. - forceUnmountCurrentAndReconcile( - current, - workInProgress, - nextChildren, - renderLanes, - ); - } else { - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - } - - // Memoize state using the values we just used to render. - // TODO: Restructure so we never read values from the instance. - workInProgress.memoizedState = instance.state; - - // The context might have changed so we need to recalculate it. - if (hasContext) { - invalidateContextProvider(workInProgress, Component, true); - } - - return workInProgress.child; -} - -function pushHostRootContext(workInProgress) { - const root = (workInProgress.stateNode: FiberRoot); - if (root.pendingContext) { - pushTopLevelContextObject( - workInProgress, - root.pendingContext, - root.pendingContext !== root.context, - ); - } else if (root.context) { - // Should always be set - pushTopLevelContextObject(workInProgress, root.context, false); - } - pushHostContainer(workInProgress, root.containerInfo); -} - -function updateHostRoot(current, workInProgress, renderLanes) { - pushHostRootContext(workInProgress); - - if (current === null) { - throw new Error('Should have a current fiber. This is a bug in React.'); - } - - const nextProps = workInProgress.pendingProps; - const prevState = workInProgress.memoizedState; - const prevChildren = prevState.element; - cloneUpdateQueue(current, workInProgress); - processUpdateQueue(workInProgress, nextProps, null, renderLanes); - - const nextState: RootState = workInProgress.memoizedState; - const root: FiberRoot = workInProgress.stateNode; - pushRootTransition(workInProgress, root, renderLanes); - - if (enableTransitionTracing) { - pushRootMarkerInstance(workInProgress); - } - - if (enableCache) { - const nextCache: Cache = nextState.cache; - pushCacheProvider(workInProgress, nextCache); - if (nextCache !== prevState.cache) { - // The root cache refreshed. - propagateContextChange(workInProgress, CacheContext, renderLanes); - } - } - - // Caution: React DevTools currently depends on this property - // being called "element". - const nextChildren = nextState.element; - if (supportsHydration && prevState.isDehydrated) { - // This is a hydration root whose shell has not yet hydrated. We should - // attempt to hydrate. - - // Flip isDehydrated to false to indicate that when this render - // finishes, the root will no longer be dehydrated. - const overrideState: RootState = { - element: nextChildren, - isDehydrated: false, - cache: nextState.cache, - }; - const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); - // `baseState` can always be the last state because the root doesn't - // have reducer functions so it doesn't need rebasing. - updateQueue.baseState = overrideState; - workInProgress.memoizedState = overrideState; - - if (workInProgress.flags & ForceClientRender) { - // Something errored during a previous attempt to hydrate the shell, so we - // forced a client render. - const recoverableError = createCapturedValueAtFiber( - new Error( - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to ' + - 'client rendering.', - ), - workInProgress, - ); - return mountHostRootWithoutHydrating( - current, - workInProgress, - nextChildren, - renderLanes, - recoverableError, - ); - } else if (nextChildren !== prevChildren) { - const recoverableError = createCapturedValueAtFiber( - new Error( - 'This root received an early update, before anything was able ' + - 'hydrate. Switched the entire root to client rendering.', - ), - workInProgress, - ); - return mountHostRootWithoutHydrating( - current, - workInProgress, - nextChildren, - renderLanes, - recoverableError, - ); - } else { - // The outermost shell has not hydrated yet. Start hydrating. - enterHydrationState(workInProgress); - if (enableUseMutableSource) { - const mutableSourceEagerHydrationData = - root.mutableSourceEagerHydrationData; - if (mutableSourceEagerHydrationData != null) { - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { - const mutableSource = ((mutableSourceEagerHydrationData[ - i - ]: any): MutableSource); - const version = mutableSourceEagerHydrationData[i + 1]; - setWorkInProgressVersion(mutableSource, version); - } - } - } - - const child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - workInProgress.child = child; - - let node = child; - while (node) { - // Mark each child as hydrating. This is a fast path to know whether this - // tree is part of a hydrating tree. This is used to determine if a child - // node has fully mounted yet, and for scheduling event replaying. - // Conceptually this is similar to Placement in that a new subtree is - // inserted into the React tree here. It just happens to not need DOM - // mutations because it already exists. - node.flags = (node.flags & ~Placement) | Hydrating; - node = node.sibling; - } - } - } else { - // Root is not dehydrated. Either this is a client-only root, or it - // already hydrated. - resetHydrationState(); - if (nextChildren === prevChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - } - return workInProgress.child; -} - -function mountHostRootWithoutHydrating( - current: Fiber, - workInProgress: Fiber, - nextChildren: ReactNodeList, - renderLanes: Lanes, - recoverableError: CapturedValue, -) { - // Revert to client rendering. - resetHydrationState(); - - queueHydrationError(recoverableError); - - workInProgress.flags |= ForceClientRender; - - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateHostComponent( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - pushHostContext(workInProgress); - - if (current === null) { - tryToClaimNextHydratableInstance(workInProgress); - } - - const type = workInProgress.type; - const nextProps = workInProgress.pendingProps; - const prevProps = current !== null ? current.memoizedProps : null; - - let nextChildren = nextProps.children; - const isDirectTextChild = shouldSetTextContent(type, nextProps); - - if (isDirectTextChild) { - // We special case a direct text child of a host node. This is a common - // case. We won't handle it as a reified child. We will instead handle - // this in the host environment that also has access to this prop. That - // avoids allocating another HostText fiber and traversing it. - nextChildren = null; - } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { - // If we're switching from a direct text child to a normal child, or to - // empty, we need to schedule the text content to be reset. - workInProgress.flags |= ContentReset; - } - - markRef(current, workInProgress); - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -function updateHostResource(current, workInProgress, renderLanes) { - pushHostContext(workInProgress); - markRef(current, workInProgress); - const currentProps = current === null ? null : current.memoizedProps; - workInProgress.memoizedState = getResource( - workInProgress.type, - workInProgress.pendingProps, - currentProps, - ); - // Resources never have reconciler managed children. It is possible for - // the host implementation of getResource to consider children in the - // resource construction but they will otherwise be discarded. In practice - // this precludes all but the simplest children and Host specific warnings - // should be implemented to warn when children are passsed when otherwise not - // expected - return null; -} - -function updateHostSingleton( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - pushHostContext(workInProgress); - - if (current === null) { - claimHydratableSingleton(workInProgress); - } - - const nextChildren = workInProgress.pendingProps.children; - - if (current === null && !getIsHydrating()) { - // Similar to Portals we append Singleton children in the commit phase. So we - // Track insertions even on mount. - // TODO: Consider unifying this with how the root works. - workInProgress.child = reconcileChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - } else { - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - } - markRef(current, workInProgress); - return workInProgress.child; -} - -function updateHostText(current, workInProgress) { - if (current === null) { - tryToClaimNextHydratableInstance(workInProgress); - } - // Nothing to do here. This is terminal. We'll do the completion step - // immediately after. - return null; -} - -function mountLazyComponent( - _current, - workInProgress, - elementType, - renderLanes, -) { - resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); - - const props = workInProgress.pendingProps; - const lazyComponent: LazyComponentType = elementType; - const payload = lazyComponent._payload; - const init = lazyComponent._init; - let Component = init(payload); - // Store the unwrapped component in the type. - workInProgress.type = Component; - const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component)); - const resolvedProps = resolveDefaultProps(Component, props); - let child; - switch (resolvedTag) { - case FunctionComponent: { - if (__DEV__) { - validateFunctionComponentInDev(workInProgress, Component); - workInProgress.type = Component = resolveFunctionForHotReloading( - Component, - ); - } - child = updateFunctionComponent( - null, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - return child; - } - case ClassComponent: { - if (__DEV__) { - workInProgress.type = Component = resolveClassForHotReloading( - Component, - ); - } - child = updateClassComponent( - null, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - return child; - } - case ForwardRef: { - if (__DEV__) { - workInProgress.type = Component = resolveForwardRefForHotReloading( - Component, - ); - } - child = updateForwardRef( - null, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - return child; - } - case MemoComponent: { - if (__DEV__) { - if (workInProgress.type !== workInProgress.elementType) { - const outerPropTypes = Component.propTypes; - if (outerPropTypes) { - checkPropTypes( - outerPropTypes, - resolvedProps, // Resolved for outer only - 'prop', - getComponentNameFromType(Component), - ); - } - } - } - child = updateMemoComponent( - null, - workInProgress, - Component, - resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too - renderLanes, - ); - return child; - } - } - let hint = ''; - if (__DEV__) { - if ( - Component !== null && - typeof Component === 'object' && - Component.$$typeof === REACT_LAZY_TYPE - ) { - hint = ' Did you wrap a component in React.lazy() more than once?'; - } - } - - // This message intentionally doesn't mention ForwardRef or MemoComponent - // because the fact that it's a separate type of work is an - // implementation detail. - throw new Error( - `Element type is invalid. Received a promise that resolves to: ${Component}. ` + - `Lazy element type must resolve to a class or function.${hint}`, - ); -} - -function mountIncompleteClassComponent( - _current, - workInProgress, - Component, - nextProps, - renderLanes, -) { - resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); - - // Promote the fiber to a class and try rendering again. - workInProgress.tag = ClassComponent; - - // The rest of this function is a fork of `updateClassComponent` - - // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - let hasContext; - if (isLegacyContextProvider(Component)) { - hasContext = true; - pushLegacyContextProvider(workInProgress); - } else { - hasContext = false; - } - prepareToReadContext(workInProgress, renderLanes); - - constructClassInstance(workInProgress, Component, nextProps); - mountClassInstance(workInProgress, Component, nextProps, renderLanes); - - return finishClassComponent( - null, - workInProgress, - Component, - true, - hasContext, - renderLanes, - ); -} - -function mountIndeterminateComponent( - _current, - workInProgress, - Component, - renderLanes, -) { - resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); - - const props = workInProgress.pendingProps; - let context; - if (!disableLegacyContext) { - const unmaskedContext = getUnmaskedContext( - workInProgress, - Component, - false, - ); - context = getMaskedContext(workInProgress, unmaskedContext); - } - - prepareToReadContext(workInProgress, renderLanes); - let value; - let hasId; - - if (enableSchedulingProfiler) { - markComponentRenderStarted(workInProgress); - } - if (__DEV__) { - if ( - Component.prototype && - typeof Component.prototype.render === 'function' - ) { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - - if (!didWarnAboutBadClass[componentName]) { - console.error( - "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + - 'This is likely to cause errors. Change %s to extend React.Component instead.', - componentName, - componentName, - ); - didWarnAboutBadClass[componentName] = true; - } - } - - if (workInProgress.mode & StrictLegacyMode) { - ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); - } - - setIsRendering(true); - ReactCurrentOwner.current = workInProgress; - value = renderWithHooks( - null, - workInProgress, - Component, - props, - context, - renderLanes, - ); - hasId = checkDidRenderIdHook(); - setIsRendering(false); - } else { - value = renderWithHooks( - null, - workInProgress, - Component, - props, - context, - renderLanes, - ); - hasId = checkDidRenderIdHook(); - } - if (enableSchedulingProfiler) { - markComponentRenderStopped(); - } - - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - - if (__DEV__) { - // Support for module components is deprecated and is removed behind a flag. - // Whether or not it would crash later, we want to show a good message in DEV first. - if ( - typeof value === 'object' && - value !== null && - typeof value.render === 'function' && - value.$$typeof === undefined - ) { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - if (!didWarnAboutModulePatternComponent[componentName]) { - console.error( - 'The <%s /> component appears to be a function component that returns a class instance. ' + - 'Change %s to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + - 'cannot be called with `new` by React.', - componentName, - componentName, - componentName, - ); - didWarnAboutModulePatternComponent[componentName] = true; - } - } - } - - if ( - // Run these checks in production only if the flag is off. - // Eventually we'll delete this branch altogether. - !disableModulePatternComponents && - typeof value === 'object' && - value !== null && - typeof value.render === 'function' && - value.$$typeof === undefined - ) { - if (__DEV__) { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - if (!didWarnAboutModulePatternComponent[componentName]) { - console.error( - 'The <%s /> component appears to be a function component that returns a class instance. ' + - 'Change %s to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + - 'cannot be called with `new` by React.', - componentName, - componentName, - componentName, - ); - didWarnAboutModulePatternComponent[componentName] = true; - } - } - - // Proceed under the assumption that this is a class instance - workInProgress.tag = ClassComponent; - - // Throw out any hooks that were used. - workInProgress.memoizedState = null; - workInProgress.updateQueue = null; - - // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - let hasContext = false; - if (isLegacyContextProvider(Component)) { - hasContext = true; - pushLegacyContextProvider(workInProgress); - } else { - hasContext = false; - } - - workInProgress.memoizedState = - value.state !== null && value.state !== undefined ? value.state : null; - - initializeUpdateQueue(workInProgress); - - adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, Component, props, renderLanes); - return finishClassComponent( - null, - workInProgress, - Component, - true, - hasContext, - renderLanes, - ); - } else { - // Proceed under the assumption that this is a function component - workInProgress.tag = FunctionComponent; - if (__DEV__) { - if (disableLegacyContext && Component.contextTypes) { - console.error( - '%s uses the legacy contextTypes API which is no longer supported. ' + - 'Use React.createContext() with React.useContext() instead.', - getComponentNameFromType(Component) || 'Unknown', - ); - } - } - - if (getIsHydrating() && hasId) { - pushMaterializedTreeId(workInProgress); - } - - reconcileChildren(null, workInProgress, value, renderLanes); - if (__DEV__) { - validateFunctionComponentInDev(workInProgress, Component); - } - return workInProgress.child; - } -} - -function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { - if (__DEV__) { - if (Component) { - if (Component.childContextTypes) { - console.error( - '%s(...): childContextTypes cannot be defined on a function component.', - Component.displayName || Component.name || 'Component', - ); - } - } - if (workInProgress.ref !== null) { - let info = ''; - const ownerName = getCurrentFiberOwnerNameInDevOrNull(); - if (ownerName) { - info += '\n\nCheck the render method of `' + ownerName + '`.'; - } - - let warningKey = ownerName || ''; - const debugSource = workInProgress._debugSource; - if (debugSource) { - warningKey = debugSource.fileName + ':' + debugSource.lineNumber; - } - if (!didWarnAboutFunctionRefs[warningKey]) { - didWarnAboutFunctionRefs[warningKey] = true; - console.error( - 'Function components cannot be given refs. ' + - 'Attempts to access this ref will fail. ' + - 'Did you mean to use React.forwardRef()?%s', - info, - ); - } - } - - if ( - warnAboutDefaultPropsOnFunctionComponents && - Component.defaultProps !== undefined - ) { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - - if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { - console.error( - '%s: Support for defaultProps will be removed from function components ' + - 'in a future major release. Use JavaScript default parameters instead.', - componentName, - ); - didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; - } - } - - if (typeof Component.getDerivedStateFromProps === 'function') { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - - if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) { - console.error( - '%s: Function components do not support getDerivedStateFromProps.', - componentName, - ); - didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true; - } - } - - if ( - typeof Component.contextType === 'object' && - Component.contextType !== null - ) { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - - if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) { - console.error( - '%s: Function components do not support contextType.', - componentName, - ); - didWarnAboutContextTypeOnFunctionComponent[componentName] = true; - } - } - } -} - -const SUSPENDED_MARKER: SuspenseState = { - dehydrated: null, - treeContext: null, - retryLane: NoLane, -}; - -function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState { - return { - baseLanes: renderLanes, - cachePool: getSuspendedCache(), - }; -} - -function updateSuspenseOffscreenState( - prevOffscreenState: OffscreenState, - renderLanes: Lanes, -): OffscreenState { - let cachePool: SpawnedCachePool | null = null; - if (enableCache) { - const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool; - if (prevCachePool !== null) { - const parentCache = isPrimaryRenderer - ? CacheContext._currentValue - : CacheContext._currentValue2; - if (prevCachePool.parent !== parentCache) { - // Detected a refresh in the parent. This overrides any previously - // suspended cache. - cachePool = { - parent: parentCache, - pool: parentCache, - }; - } else { - // We can reuse the cache from last time. The only thing that would have - // overridden it is a parent refresh, which we checked for above. - cachePool = prevCachePool; - } - } else { - // If there's no previous cache pool, grab the current one. - cachePool = getSuspendedCache(); - } - } - return { - baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes), - cachePool, - }; -} - -// TODO: Probably should inline this back -function shouldRemainOnFallback( - current: null | Fiber, - workInProgress: Fiber, - renderLanes: Lanes, -) { - // If we're already showing a fallback, there are cases where we need to - // remain on that fallback regardless of whether the content has resolved. - // For example, SuspenseList coordinates when nested content appears. - if (current !== null) { - const suspenseState: SuspenseState = current.memoizedState; - if (suspenseState === null) { - // Currently showing content. Don't hide it, even if ForceSuspenseFallback - // is true. More precise name might be "ForceRemainSuspenseFallback". - // Note: This is a factoring smell. Can't remain on a fallback if there's - // no fallback to remain on. - return false; - } - } - - // Not currently showing content. Consult the Suspense context. - const suspenseContext: SuspenseContext = suspenseStackCursor.current; - return hasSuspenseListContext( - suspenseContext, - (ForceSuspenseFallback: SuspenseContext), - ); -} - -function getRemainingWorkInPrimaryTree(current: Fiber, renderLanes) { - // TODO: Should not remove render lanes that were pinged during this render - return removeLanes(current.childLanes, renderLanes); -} - -function updateSuspenseComponent(current, workInProgress, renderLanes) { - const nextProps = workInProgress.pendingProps; - - // This is used by DevTools to force a boundary to suspend. - if (__DEV__) { - if (shouldSuspend(workInProgress)) { - workInProgress.flags |= DidCapture; - } - } - - let showFallback = false; - const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags; - if ( - didSuspend || - shouldRemainOnFallback(current, workInProgress, renderLanes) - ) { - // Something in this boundary's subtree already suspended. Switch to - // rendering the fallback children. - showFallback = true; - workInProgress.flags &= ~DidCapture; - } - - // OK, the next part is confusing. We're about to reconcile the Suspense - // boundary's children. This involves some custom reconciliation logic. Two - // main reasons this is so complicated. - // - // First, Legacy Mode has different semantics for backwards compatibility. The - // primary tree will commit in an inconsistent state, so when we do the - // second pass to render the fallback, we do some exceedingly, uh, clever - // hacks to make that not totally break. Like transferring effects and - // deletions from hidden tree. In Concurrent Mode, it's much simpler, - // because we bailout on the primary tree completely and leave it in its old - // state, no effects. Same as what we do for Offscreen (except that - // Offscreen doesn't have the first render pass). - // - // Second is hydration. During hydration, the Suspense fiber has a slightly - // different layout, where the child points to a dehydrated fragment, which - // contains the DOM rendered by the server. - // - // Third, even if you set all that aside, Suspense is like error boundaries in - // that we first we try to render one tree, and if that fails, we render again - // and switch to a different tree. Like a try/catch block. So we have to track - // which branch we're currently rendering. Ideally we would model this using - // a stack. - if (current === null) { - // Initial mount - - // Special path for hydration - // If we're currently hydrating, try to hydrate this boundary. - if (getIsHydrating()) { - // We must push the suspense handler context *before* attempting to - // hydrate, to avoid a mismatch in case it errors. - if (showFallback) { - pushPrimaryTreeSuspenseHandler(workInProgress); - } else { - pushFallbackTreeSuspenseHandler(workInProgress); - } - tryToClaimNextHydratableInstance(workInProgress); - // This could've been a dehydrated suspense component. - const suspenseState: null | SuspenseState = workInProgress.memoizedState; - if (suspenseState !== null) { - const dehydrated = suspenseState.dehydrated; - if (dehydrated !== null) { - return mountDehydratedSuspenseComponent( - workInProgress, - dehydrated, - renderLanes, - ); - } - } - // If hydration didn't succeed, fall through to the normal Suspense path. - // To avoid a stack mismatch we need to pop the Suspense handler that we - // pushed above. This will become less awkward when move the hydration - // logic to its own fiber. - popSuspenseHandler(workInProgress); - } - - const nextPrimaryChildren = nextProps.children; - const nextFallbackChildren = nextProps.fallback; - - if (showFallback) { - pushFallbackTreeSuspenseHandler(workInProgress); - - const fallbackFragment = mountSuspenseFallbackChildren( - workInProgress, - nextPrimaryChildren, - nextFallbackChildren, - renderLanes, - ); - const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = mountSuspenseOffscreenState( - renderLanes, - ); - workInProgress.memoizedState = SUSPENDED_MARKER; - if (enableTransitionTracing) { - const currentTransitions = getPendingTransitions(); - if (currentTransitions !== null) { - const parentMarkerInstances = getMarkerInstances(); - const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any); - if (offscreenQueue === null) { - const newOffscreenQueue: OffscreenQueue = { - transitions: currentTransitions, - markerInstances: parentMarkerInstances, - wakeables: null, - }; - primaryChildFragment.updateQueue = newOffscreenQueue; - } else { - offscreenQueue.transitions = currentTransitions; - offscreenQueue.markerInstances = parentMarkerInstances; - } - } - } - - return fallbackFragment; - } else if ( - enableCPUSuspense && - typeof nextProps.unstable_expectedLoadTime === 'number' - ) { - // This is a CPU-bound tree. Skip this tree and show a placeholder to - // unblock the surrounding content. Then immediately retry after the - // initial commit. - pushFallbackTreeSuspenseHandler(workInProgress); - const fallbackFragment = mountSuspenseFallbackChildren( - workInProgress, - nextPrimaryChildren, - nextFallbackChildren, - renderLanes, - ); - const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = mountSuspenseOffscreenState( - renderLanes, - ); - workInProgress.memoizedState = SUSPENDED_MARKER; - - // TODO: Transition Tracing is not yet implemented for CPU Suspense. - - // Since nothing actually suspended, there will nothing to ping this to - // get it started back up to attempt the next item. While in terms of - // priority this work has the same priority as this current render, it's - // not part of the same transition once the transition has committed. If - // it's sync, we still want to yield so that it can be painted. - // Conceptually, this is really the same as pinging. We can use any - // RetryLane even if it's the one currently rendering since we're leaving - // it behind on this node. - workInProgress.lanes = SomeRetryLane; - return fallbackFragment; - } else { - pushPrimaryTreeSuspenseHandler(workInProgress); - return mountSuspensePrimaryChildren( - workInProgress, - nextPrimaryChildren, - renderLanes, - ); - } - } else { - // This is an update. - - // Special path for hydration - const prevState: null | SuspenseState = current.memoizedState; - if (prevState !== null) { - const dehydrated = prevState.dehydrated; - if (dehydrated !== null) { - return updateDehydratedSuspenseComponent( - current, - workInProgress, - didSuspend, - nextProps, - dehydrated, - prevState, - renderLanes, - ); - } - } - - if (showFallback) { - pushFallbackTreeSuspenseHandler(workInProgress); - - const nextFallbackChildren = nextProps.fallback; - const nextPrimaryChildren = nextProps.children; - const fallbackChildFragment = updateSuspenseFallbackChildren( - current, - workInProgress, - nextPrimaryChildren, - nextFallbackChildren, - renderLanes, - ); - const primaryChildFragment: Fiber = (workInProgress.child: any); - const prevOffscreenState: OffscreenState | null = (current.child: any) - .memoizedState; - primaryChildFragment.memoizedState = - prevOffscreenState === null - ? mountSuspenseOffscreenState(renderLanes) - : updateSuspenseOffscreenState(prevOffscreenState, renderLanes); - if (enableTransitionTracing) { - const currentTransitions = getPendingTransitions(); - if (currentTransitions !== null) { - const parentMarkerInstances = getMarkerInstances(); - const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any); - const currentOffscreenQueue: OffscreenQueue | null = (current.updateQueue: any); - if (offscreenQueue === null) { - const newOffscreenQueue: OffscreenQueue = { - transitions: currentTransitions, - markerInstances: parentMarkerInstances, - wakeables: null, - }; - primaryChildFragment.updateQueue = newOffscreenQueue; - } else if (offscreenQueue === currentOffscreenQueue) { - // If the work-in-progress queue is the same object as current, we - // can't modify it without cloning it first. - const newOffscreenQueue: OffscreenQueue = { - transitions: currentTransitions, - markerInstances: parentMarkerInstances, - wakeables: - currentOffscreenQueue !== null - ? currentOffscreenQueue.wakeables - : null, - }; - primaryChildFragment.updateQueue = newOffscreenQueue; - } else { - offscreenQueue.transitions = currentTransitions; - offscreenQueue.markerInstances = parentMarkerInstances; - } - } - } - primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree( - current, - renderLanes, - ); - workInProgress.memoizedState = SUSPENDED_MARKER; - return fallbackChildFragment; - } else { - pushPrimaryTreeSuspenseHandler(workInProgress); - - const nextPrimaryChildren = nextProps.children; - const primaryChildFragment = updateSuspensePrimaryChildren( - current, - workInProgress, - nextPrimaryChildren, - renderLanes, - ); - workInProgress.memoizedState = null; - return primaryChildFragment; - } - } -} - -function mountSuspensePrimaryChildren( - workInProgress, - primaryChildren, - renderLanes, -) { - const mode = workInProgress.mode; - const primaryChildProps: OffscreenProps = { - mode: 'visible', - children: primaryChildren, - }; - const primaryChildFragment = mountWorkInProgressOffscreenFiber( - primaryChildProps, - mode, - renderLanes, - ); - primaryChildFragment.return = workInProgress; - workInProgress.child = primaryChildFragment; - return primaryChildFragment; -} - -function mountSuspenseFallbackChildren( - workInProgress, - primaryChildren, - fallbackChildren, - renderLanes, -) { - const mode = workInProgress.mode; - const progressedPrimaryFragment: Fiber | null = workInProgress.child; - - const primaryChildProps: OffscreenProps = { - mode: 'hidden', - children: primaryChildren, - }; - - let primaryChildFragment; - let fallbackChildFragment; - if ( - (mode & ConcurrentMode) === NoMode && - progressedPrimaryFragment !== null - ) { - // In legacy mode, we commit the primary tree as if it successfully - // completed, even though it's in an inconsistent state. - primaryChildFragment = progressedPrimaryFragment; - primaryChildFragment.childLanes = NoLanes; - primaryChildFragment.pendingProps = primaryChildProps; - - if (enableProfilerTimer && workInProgress.mode & ProfileMode) { - // Reset the durations from the first pass so they aren't included in the - // final amounts. This seems counterintuitive, since we're intentionally - // not measuring part of the render phase, but this makes it match what we - // do in Concurrent Mode. - primaryChildFragment.actualDuration = 0; - primaryChildFragment.actualStartTime = -1; - primaryChildFragment.selfBaseDuration = 0; - primaryChildFragment.treeBaseDuration = 0; - } - - fallbackChildFragment = createFiberFromFragment( - fallbackChildren, - mode, - renderLanes, - null, - ); - } else { - primaryChildFragment = mountWorkInProgressOffscreenFiber( - primaryChildProps, - mode, - NoLanes, - ); - fallbackChildFragment = createFiberFromFragment( - fallbackChildren, - mode, - renderLanes, - null, - ); - } - - primaryChildFragment.return = workInProgress; - fallbackChildFragment.return = workInProgress; - primaryChildFragment.sibling = fallbackChildFragment; - workInProgress.child = primaryChildFragment; - return fallbackChildFragment; -} - -function mountWorkInProgressOffscreenFiber( - offscreenProps: OffscreenProps, - mode: TypeOfMode, - renderLanes: Lanes, -) { - // The props argument to `createFiberFromOffscreen` is `any` typed, so we use - // this wrapper function to constrain it. - return createFiberFromOffscreen(offscreenProps, mode, NoLanes, null); -} - -function updateWorkInProgressOffscreenFiber( - current: Fiber, - offscreenProps: OffscreenProps, -) { - // The props argument to `createWorkInProgress` is `any` typed, so we use this - // wrapper function to constrain it. - return createWorkInProgress(current, offscreenProps); -} - -function updateSuspensePrimaryChildren( - current, - workInProgress, - primaryChildren, - renderLanes, -) { - const currentPrimaryChildFragment: Fiber = (current.child: any); - const currentFallbackChildFragment: Fiber | null = - currentPrimaryChildFragment.sibling; - - const primaryChildFragment = updateWorkInProgressOffscreenFiber( - currentPrimaryChildFragment, - { - mode: 'visible', - children: primaryChildren, - }, - ); - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - primaryChildFragment.lanes = renderLanes; - } - primaryChildFragment.return = workInProgress; - primaryChildFragment.sibling = null; - if (currentFallbackChildFragment !== null) { - // Delete the fallback child fragment - const deletions = workInProgress.deletions; - if (deletions === null) { - workInProgress.deletions = [currentFallbackChildFragment]; - workInProgress.flags |= ChildDeletion; - } else { - deletions.push(currentFallbackChildFragment); - } - } - - workInProgress.child = primaryChildFragment; - return primaryChildFragment; -} - -function updateSuspenseFallbackChildren( - current, - workInProgress, - primaryChildren, - fallbackChildren, - renderLanes, -) { - const mode = workInProgress.mode; - const currentPrimaryChildFragment: Fiber = (current.child: any); - const currentFallbackChildFragment: Fiber | null = - currentPrimaryChildFragment.sibling; - - const primaryChildProps: OffscreenProps = { - mode: 'hidden', - children: primaryChildren, - }; - - let primaryChildFragment; - if ( - // In legacy mode, we commit the primary tree as if it successfully - // completed, even though it's in an inconsistent state. - (mode & ConcurrentMode) === NoMode && - // Make sure we're on the second pass, i.e. the primary child fragment was - // already cloned. In legacy mode, the only case where this isn't true is - // when DevTools forces us to display a fallback; we skip the first render - // pass entirely and go straight to rendering the fallback. (In Concurrent - // Mode, SuspenseList can also trigger this scenario, but this is a legacy- - // only codepath.) - workInProgress.child !== currentPrimaryChildFragment - ) { - const progressedPrimaryFragment: Fiber = (workInProgress.child: any); - primaryChildFragment = progressedPrimaryFragment; - primaryChildFragment.childLanes = NoLanes; - primaryChildFragment.pendingProps = primaryChildProps; - - if (enableProfilerTimer && workInProgress.mode & ProfileMode) { - // Reset the durations from the first pass so they aren't included in the - // final amounts. This seems counterintuitive, since we're intentionally - // not measuring part of the render phase, but this makes it match what we - // do in Concurrent Mode. - primaryChildFragment.actualDuration = 0; - primaryChildFragment.actualStartTime = -1; - primaryChildFragment.selfBaseDuration = - currentPrimaryChildFragment.selfBaseDuration; - primaryChildFragment.treeBaseDuration = - currentPrimaryChildFragment.treeBaseDuration; - } - - // The fallback fiber was added as a deletion during the first pass. - // However, since we're going to remain on the fallback, we no longer want - // to delete it. - workInProgress.deletions = null; - } else { - primaryChildFragment = updateWorkInProgressOffscreenFiber( - currentPrimaryChildFragment, - primaryChildProps, - ); - // Since we're reusing a current tree, we need to reuse the flags, too. - // (We don't do this in legacy mode, because in legacy mode we don't re-use - // the current tree; see previous branch.) - primaryChildFragment.subtreeFlags = - currentPrimaryChildFragment.subtreeFlags & StaticMask; - } - let fallbackChildFragment; - if (currentFallbackChildFragment !== null) { - fallbackChildFragment = createWorkInProgress( - currentFallbackChildFragment, - fallbackChildren, - ); - } else { - fallbackChildFragment = createFiberFromFragment( - fallbackChildren, - mode, - renderLanes, - null, - ); - // Needs a placement effect because the parent (the Suspense boundary) already - // mounted but this is a new fiber. - fallbackChildFragment.flags |= Placement; - } - - fallbackChildFragment.return = workInProgress; - primaryChildFragment.return = workInProgress; - primaryChildFragment.sibling = fallbackChildFragment; - workInProgress.child = primaryChildFragment; - - return fallbackChildFragment; -} - -function retrySuspenseComponentWithoutHydrating( - current: Fiber, - workInProgress: Fiber, - renderLanes: Lanes, - recoverableError: CapturedValue | null, -) { - // Falling back to client rendering. Because this has performance - // implications, it's considered a recoverable error, even though the user - // likely won't observe anything wrong with the UI. - // - // The error is passed in as an argument to enforce that every caller provide - // a custom message, or explicitly opt out (currently the only path that opts - // out is legacy mode; every concurrent path provides an error). - if (recoverableError !== null) { - queueHydrationError(recoverableError); - } - - // This will add the old fiber to the deletion list - reconcileChildFibers(workInProgress, current.child, null, renderLanes); - - // We're now not suspended nor dehydrated. - const nextProps = workInProgress.pendingProps; - const primaryChildren = nextProps.children; - const primaryChildFragment = mountSuspensePrimaryChildren( - workInProgress, - primaryChildren, - renderLanes, - ); - // Needs a placement effect because the parent (the Suspense boundary) already - // mounted but this is a new fiber. - primaryChildFragment.flags |= Placement; - workInProgress.memoizedState = null; - - return primaryChildFragment; -} - -function mountSuspenseFallbackAfterRetryWithoutHydrating( - current, - workInProgress, - primaryChildren, - fallbackChildren, - renderLanes, -) { - const fiberMode = workInProgress.mode; - const primaryChildProps: OffscreenProps = { - mode: 'visible', - children: primaryChildren, - }; - const primaryChildFragment = mountWorkInProgressOffscreenFiber( - primaryChildProps, - fiberMode, - NoLanes, - ); - const fallbackChildFragment = createFiberFromFragment( - fallbackChildren, - fiberMode, - renderLanes, - null, - ); - // Needs a placement effect because the parent (the Suspense - // boundary) already mounted but this is a new fiber. - fallbackChildFragment.flags |= Placement; - - primaryChildFragment.return = workInProgress; - fallbackChildFragment.return = workInProgress; - primaryChildFragment.sibling = fallbackChildFragment; - workInProgress.child = primaryChildFragment; - - if ((workInProgress.mode & ConcurrentMode) !== NoMode) { - // We will have dropped the effect list which contains the - // deletion. We need to reconcile to delete the current child. - reconcileChildFibers(workInProgress, current.child, null, renderLanes); - } - - return fallbackChildFragment; -} - -function mountDehydratedSuspenseComponent( - workInProgress: Fiber, - suspenseInstance: SuspenseInstance, - renderLanes: Lanes, -): null | Fiber { - // During the first pass, we'll bail out and not drill into the children. - // Instead, we'll leave the content in place and try to hydrate it later. - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - if (__DEV__) { - console.error( - 'Cannot hydrate Suspense in legacy mode. Switch from ' + - 'ReactDOM.hydrate(element, container) to ' + - 'ReactDOMClient.hydrateRoot(container, )' + - '.render(element) or remove the Suspense components from ' + - 'the server rendered components.', - ); - } - workInProgress.lanes = laneToLanes(SyncLane); - } else if (isSuspenseInstanceFallback(suspenseInstance)) { - // This is a client-only boundary. Since we won't get any content from the server - // for this, we need to schedule that at a higher priority based on when it would - // have timed out. In theory we could render it in this pass but it would have the - // wrong priority associated with it and will prevent hydration of parent path. - // Instead, we'll leave work left on it to render it in a separate commit. - - // TODO This time should be the time at which the server rendered response that is - // a parent to this boundary was displayed. However, since we currently don't have - // a protocol to transfer that time, we'll just estimate it by using the current - // time. This will mean that Suspense timeouts are slightly shifted to later than - // they should be. - // Schedule a normal pri update to render this content. - workInProgress.lanes = laneToLanes(DefaultHydrationLane); - } else { - // We'll continue hydrating the rest at offscreen priority since we'll already - // be showing the right content coming from the server, it is no rush. - workInProgress.lanes = laneToLanes(OffscreenLane); - } - return null; -} - -function updateDehydratedSuspenseComponent( - current: Fiber, - workInProgress: Fiber, - didSuspend: boolean, - nextProps: any, - suspenseInstance: SuspenseInstance, - suspenseState: SuspenseState, - renderLanes: Lanes, -): null | Fiber { - if (!didSuspend) { - // This is the first render pass. Attempt to hydrate. - pushPrimaryTreeSuspenseHandler(workInProgress); - - // We should never be hydrating at this point because it is the first pass, - // but after we've already committed once. - warnIfHydrating(); - - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, - null, - ); - } - - if (isSuspenseInstanceFallback(suspenseInstance)) { - // This boundary is in a permanent fallback state. In this case, we'll never - // get an update and we'll never be able to hydrate the final content. Let's just try the - // client side render instead. - let digest, message, stack; - if (__DEV__) { - ({digest, message, stack} = getSuspenseInstanceFallbackErrorDetails( - suspenseInstance, - )); - } else { - ({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance)); - } - - let error; - if (message) { - // eslint-disable-next-line react-internal/prod-error-codes - error = new Error(message); - } else { - error = new Error( - 'The server could not finish this Suspense boundary, likely ' + - 'due to an error during server rendering. Switched to ' + - 'client rendering.', - ); - } - (error: any).digest = digest; - const capturedValue = createCapturedValue(error, digest, stack); - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, - capturedValue, - ); - } - - if ( - enableLazyContextPropagation && - // TODO: Factoring is a little weird, since we check this right below, too. - // But don't want to re-arrange the if-else chain until/unless this - // feature lands. - !didReceiveUpdate - ) { - // We need to check if any children have context before we decide to bail - // out, so propagate the changes now. - lazilyPropagateParentContextChanges(current, workInProgress, renderLanes); - } - - // We use lanes to indicate that a child might depend on context, so if - // any context has changed, we need to treat is as if the input might have changed. - const hasContextChanged = includesSomeLane(renderLanes, current.childLanes); - if (didReceiveUpdate || hasContextChanged) { - // This boundary has changed since the first render. This means that we are now unable to - // hydrate it. We might still be able to hydrate it using a higher priority lane. - const root = getWorkInProgressRoot(); - if (root !== null) { - const attemptHydrationAtLane = getBumpedLaneForHydration( - root, - renderLanes, - ); - if ( - attemptHydrationAtLane !== NoLane && - attemptHydrationAtLane !== suspenseState.retryLane - ) { - // Intentionally mutating since this render will get interrupted. This - // is one of the very rare times where we mutate the current tree - // during the render phase. - suspenseState.retryLane = attemptHydrationAtLane; - // TODO: Ideally this would inherit the event time of the current render - const eventTime = NoTimestamp; - enqueueConcurrentRenderForLane(current, attemptHydrationAtLane); - scheduleUpdateOnFiber( - root, - current, - attemptHydrationAtLane, - eventTime, - ); - - // Throw a special object that signals to the work loop that it should - // interrupt the current render. - // - // Because we're inside a React-only execution stack, we don't - // strictly need to throw here — we could instead modify some internal - // work loop state. But using an exception means we don't need to - // check for this case on every iteration of the work loop. So doing - // it this way moves the check out of the fast path. - throw SelectiveHydrationException; - } else { - // We have already tried to ping at a higher priority than we're rendering with - // so if we got here, we must have failed to hydrate at those levels. We must - // now give up. Instead, we're going to delete the whole subtree and instead inject - // a new real Suspense boundary to take its place, which may render content - // or fallback. This might suspend for a while and if it does we might still have - // an opportunity to hydrate before this pass commits. - } - } - - // If we did not selectively hydrate, we'll continue rendering without - // hydrating. Mark this tree as suspended to prevent it from committing - // outside a transition. - // - // This path should only happen if the hydration lane already suspended. - // Currently, it also happens during sync updates because there is no - // hydration lane for sync updates. - // TODO: We should ideally have a sync hydration lane that we can apply to do - // a pass where we hydrate this subtree in place using the previous Context and then - // reapply the update afterwards. - renderDidSuspendDelayIfPossible(); - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, - null, - ); - } else if (isSuspenseInstancePending(suspenseInstance)) { - // This component is still pending more data from the server, so we can't hydrate its - // content. We treat it as if this component suspended itself. It might seem as if - // we could just try to render it client-side instead. However, this will perform a - // lot of unnecessary work and is unlikely to complete since it often will suspend - // on missing data anyway. Additionally, the server might be able to render more - // than we can on the client yet. In that case we'd end up with more fallback states - // on the client than if we just leave it alone. If the server times out or errors - // these should update this boundary to the permanent Fallback state instead. - // Mark it as having captured (i.e. suspended). - workInProgress.flags |= DidCapture; - // Leave the child in place. I.e. the dehydrated fragment. - workInProgress.child = current.child; - // Register a callback to retry this boundary once the server has sent the result. - const retry = retryDehydratedSuspenseBoundary.bind(null, current); - registerSuspenseInstanceRetry(suspenseInstance, retry); - return null; - } else { - // This is the first attempt. - reenterHydrationStateFromDehydratedSuspenseInstance( - workInProgress, - suspenseInstance, - suspenseState.treeContext, - ); - const primaryChildren = nextProps.children; - const primaryChildFragment = mountSuspensePrimaryChildren( - workInProgress, - primaryChildren, - renderLanes, - ); - // Mark the children as hydrating. This is a fast path to know whether this - // tree is part of a hydrating tree. This is used to determine if a child - // node has fully mounted yet, and for scheduling event replaying. - // Conceptually this is similar to Placement in that a new subtree is - // inserted into the React tree here. It just happens to not need DOM - // mutations because it already exists. - primaryChildFragment.flags |= Hydrating; - return primaryChildFragment; - } - } else { - // This is the second render pass. We already attempted to hydrated, but - // something either suspended or errored. - - if (workInProgress.flags & ForceClientRender) { - // Something errored during hydration. Try again without hydrating. - pushPrimaryTreeSuspenseHandler(workInProgress); - - workInProgress.flags &= ~ForceClientRender; - const capturedValue = createCapturedValue( - new Error( - 'There was an error while hydrating this Suspense boundary. ' + - 'Switched to client rendering.', - ), - ); - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, - capturedValue, - ); - } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) { - // Something suspended and we should still be in dehydrated mode. - // Leave the existing child in place. - - // Push to avoid a mismatch - pushFallbackTreeSuspenseHandler(workInProgress); - - workInProgress.child = current.child; - // The dehydrated completion pass expects this flag to be there - // but the normal suspense pass doesn't. - workInProgress.flags |= DidCapture; - return null; - } else { - // Suspended but we should no longer be in dehydrated mode. - // Therefore we now have to render the fallback. - pushFallbackTreeSuspenseHandler(workInProgress); - - const nextPrimaryChildren = nextProps.children; - const nextFallbackChildren = nextProps.fallback; - const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating( - current, - workInProgress, - nextPrimaryChildren, - nextFallbackChildren, - renderLanes, - ); - const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = mountSuspenseOffscreenState( - renderLanes, - ); - workInProgress.memoizedState = SUSPENDED_MARKER; - return fallbackChildFragment; - } - } -} - -function scheduleSuspenseWorkOnFiber( - fiber: Fiber, - renderLanes: Lanes, - propagationRoot: Fiber, -) { - fiber.lanes = mergeLanes(fiber.lanes, renderLanes); - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.lanes = mergeLanes(alternate.lanes, renderLanes); - } - scheduleContextWorkOnParentPath(fiber.return, renderLanes, propagationRoot); -} - -function propagateSuspenseContextChange( - workInProgress: Fiber, - firstChild: null | Fiber, - renderLanes: Lanes, -): void { - // Mark any Suspense boundaries with fallbacks as having work to do. - // If they were previously forced into fallbacks, they may now be able - // to unblock. - let node = firstChild; - while (node !== null) { - if (node.tag === SuspenseComponent) { - const state: SuspenseState | null = node.memoizedState; - if (state !== null) { - scheduleSuspenseWorkOnFiber(node, renderLanes, workInProgress); - } - } else if (node.tag === SuspenseListComponent) { - // If the tail is hidden there might not be an Suspense boundaries - // to schedule work on. In this case we have to schedule it on the - // list itself. - // We don't have to traverse to the children of the list since - // the list will propagate the change when it rerenders. - scheduleSuspenseWorkOnFiber(node, renderLanes, workInProgress); - } else if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - if (node === workInProgress) { - return; - } - // $FlowFixMe[incompatible-use] found when upgrading Flow - while (node.sibling === null) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - if (node.return === null || node.return === workInProgress) { - return; - } - node = node.return; - } - // $FlowFixMe[incompatible-use] found when upgrading Flow - node.sibling.return = node.return; - node = node.sibling; - } -} - -function findLastContentRow(firstChild: null | Fiber): null | Fiber { - // This is going to find the last row among these children that is already - // showing content on the screen, as opposed to being in fallback state or - // new. If a row has multiple Suspense boundaries, any of them being in the - // fallback state, counts as the whole row being in a fallback state. - // Note that the "rows" will be workInProgress, but any nested children - // will still be current since we haven't rendered them yet. The mounted - // order may not be the same as the new order. We use the new order. - let row = firstChild; - let lastContentRow: null | Fiber = null; - while (row !== null) { - const currentRow = row.alternate; - // New rows can't be content rows. - if (currentRow !== null && findFirstSuspended(currentRow) === null) { - lastContentRow = row; - } - row = row.sibling; - } - return lastContentRow; -} - -type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void; - -function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { - if (__DEV__) { - if ( - revealOrder !== undefined && - revealOrder !== 'forwards' && - revealOrder !== 'backwards' && - revealOrder !== 'together' && - !didWarnAboutRevealOrder[revealOrder] - ) { - didWarnAboutRevealOrder[revealOrder] = true; - if (typeof revealOrder === 'string') { - switch (revealOrder.toLowerCase()) { - case 'together': - case 'forwards': - case 'backwards': { - console.error( - '"%s" is not a valid value for revealOrder on . ' + - 'Use lowercase "%s" instead.', - revealOrder, - revealOrder.toLowerCase(), - ); - break; - } - case 'forward': - case 'backward': { - console.error( - '"%s" is not a valid value for revealOrder on . ' + - 'React uses the -s suffix in the spelling. Use "%ss" instead.', - revealOrder, - revealOrder.toLowerCase(), - ); - break; - } - default: - console.error( - '"%s" is not a supported revealOrder on . ' + - 'Did you mean "together", "forwards" or "backwards"?', - revealOrder, - ); - break; - } - } else { - console.error( - '%s is not a supported value for revealOrder on . ' + - 'Did you mean "together", "forwards" or "backwards"?', - revealOrder, - ); - } - } - } -} - -function validateTailOptions( - tailMode: SuspenseListTailMode, - revealOrder: SuspenseListRevealOrder, -) { - if (__DEV__) { - if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) { - if (tailMode !== 'collapsed' && tailMode !== 'hidden') { - didWarnAboutTailOptions[tailMode] = true; - console.error( - '"%s" is not a supported value for tail on . ' + - 'Did you mean "collapsed" or "hidden"?', - tailMode, - ); - } else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') { - didWarnAboutTailOptions[tailMode] = true; - console.error( - ' is only valid if revealOrder is ' + - '"forwards" or "backwards". ' + - 'Did you mean to specify revealOrder="forwards"?', - tailMode, - ); - } - } - } -} - -function validateSuspenseListNestedChild(childSlot: mixed, index: number) { - if (__DEV__) { - const isAnArray = isArray(childSlot); - const isIterable = - !isAnArray && typeof getIteratorFn(childSlot) === 'function'; - if (isAnArray || isIterable) { - const type = isAnArray ? 'array' : 'iterable'; - console.error( - 'A nested %s was passed to row #%s in . Wrap it in ' + - 'an additional SuspenseList to configure its revealOrder: ' + - ' ... ' + - '{%s} ... ' + - '', - type, - index, - type, - ); - return false; - } - } - return true; -} - -function validateSuspenseListChildren( - children: mixed, - revealOrder: SuspenseListRevealOrder, -) { - if (__DEV__) { - if ( - (revealOrder === 'forwards' || revealOrder === 'backwards') && - children !== undefined && - children !== null && - children !== false - ) { - if (isArray(children)) { - for (let i = 0; i < children.length; i++) { - if (!validateSuspenseListNestedChild(children[i], i)) { - return; - } - } - } else { - const iteratorFn = getIteratorFn(children); - if (typeof iteratorFn === 'function') { - const childrenIterator = iteratorFn.call(children); - if (childrenIterator) { - let step = childrenIterator.next(); - let i = 0; - for (; !step.done; step = childrenIterator.next()) { - if (!validateSuspenseListNestedChild(step.value, i)) { - return; - } - i++; - } - } - } else { - console.error( - 'A single row was passed to a . ' + - 'This is not useful since it needs multiple rows. ' + - 'Did you mean to pass multiple children or an array?', - revealOrder, - ); - } - } - } - } -} - -function initSuspenseListRenderState( - workInProgress: Fiber, - isBackwards: boolean, - tail: null | Fiber, - lastContentRow: null | Fiber, - tailMode: SuspenseListTailMode, -): void { - const renderState: null | SuspenseListRenderState = - workInProgress.memoizedState; - if (renderState === null) { - workInProgress.memoizedState = ({ - isBackwards: isBackwards, - rendering: null, - renderingStartTime: 0, - last: lastContentRow, - tail: tail, - tailMode: tailMode, - }: SuspenseListRenderState); - } else { - // We can reuse the existing object from previous renders. - renderState.isBackwards = isBackwards; - renderState.rendering = null; - renderState.renderingStartTime = 0; - renderState.last = lastContentRow; - renderState.tail = tail; - renderState.tailMode = tailMode; - } -} - -// This can end up rendering this component multiple passes. -// The first pass splits the children fibers into two sets. A head and tail. -// We first render the head. If anything is in fallback state, we do another -// pass through beginWork to rerender all children (including the tail) with -// the force suspend context. If the first render didn't have anything in -// in fallback state. Then we render each row in the tail one-by-one. -// That happens in the completeWork phase without going back to beginWork. -function updateSuspenseListComponent( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - const nextProps = workInProgress.pendingProps; - const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder; - const tailMode: SuspenseListTailMode = nextProps.tail; - const newChildren = nextProps.children; - - validateRevealOrder(revealOrder); - validateTailOptions(tailMode, revealOrder); - validateSuspenseListChildren(newChildren, revealOrder); - - reconcileChildren(current, workInProgress, newChildren, renderLanes); - - let suspenseContext: SuspenseContext = suspenseStackCursor.current; - - const shouldForceFallback = hasSuspenseListContext( - suspenseContext, - (ForceSuspenseFallback: SuspenseContext), - ); - if (shouldForceFallback) { - suspenseContext = setShallowSuspenseListContext( - suspenseContext, - ForceSuspenseFallback, - ); - workInProgress.flags |= DidCapture; - } else { - const didSuspendBefore = - current !== null && (current.flags & DidCapture) !== NoFlags; - if (didSuspendBefore) { - // If we previously forced a fallback, we need to schedule work - // on any nested boundaries to let them know to try to render - // again. This is the same as context updating. - propagateSuspenseContextChange( - workInProgress, - workInProgress.child, - renderLanes, - ); - } - suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext); - } - pushSuspenseListContext(workInProgress, suspenseContext); - - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - // In legacy mode, SuspenseList doesn't work so we just - // use make it a noop by treating it as the default revealOrder. - workInProgress.memoizedState = null; - } else { - switch (revealOrder) { - case 'forwards': { - const lastContentRow = findLastContentRow(workInProgress.child); - let tail; - if (lastContentRow === null) { - // The whole list is part of the tail. - // TODO: We could fast path by just rendering the tail now. - tail = workInProgress.child; - workInProgress.child = null; - } else { - // Disconnect the tail rows after the content row. - // We're going to render them separately later. - tail = lastContentRow.sibling; - lastContentRow.sibling = null; - } - initSuspenseListRenderState( - workInProgress, - false, // isBackwards - tail, - lastContentRow, - tailMode, - ); - break; - } - case 'backwards': { - // We're going to find the first row that has existing content. - // At the same time we're going to reverse the list of everything - // we pass in the meantime. That's going to be our tail in reverse - // order. - let tail = null; - let row = workInProgress.child; - workInProgress.child = null; - while (row !== null) { - const currentRow = row.alternate; - // New rows can't be content rows. - if (currentRow !== null && findFirstSuspended(currentRow) === null) { - // This is the beginning of the main content. - workInProgress.child = row; - break; - } - const nextRow = row.sibling; - row.sibling = tail; - tail = row; - row = nextRow; - } - // TODO: If workInProgress.child is null, we can continue on the tail immediately. - initSuspenseListRenderState( - workInProgress, - true, // isBackwards - tail, - null, // last - tailMode, - ); - break; - } - case 'together': { - initSuspenseListRenderState( - workInProgress, - false, // isBackwards - null, // tail - null, // last - undefined, - ); - break; - } - default: { - // The default reveal order is the same as not having - // a boundary. - workInProgress.memoizedState = null; - } - } - } - return workInProgress.child; -} - -function updatePortalComponent( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - const nextChildren = workInProgress.pendingProps; - if (current === null) { - // Portals are special because we don't append the children during mount - // but at commit. Therefore we need to track insertions which the normal - // flow doesn't do during mount. This doesn't happen at the root because - // the root always starts with a "current" with a null child. - // TODO: Consider unifying this with how the root works. - workInProgress.child = reconcileChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - } else { - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - } - return workInProgress.child; -} - -let hasWarnedAboutUsingNoValuePropOnContextProvider = false; - -function updateContextProvider( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - const providerType: ReactProviderType = workInProgress.type; - const context: ReactContext = providerType._context; - - const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; - - const newValue = newProps.value; - - if (__DEV__) { - if (!('value' in newProps)) { - if (!hasWarnedAboutUsingNoValuePropOnContextProvider) { - hasWarnedAboutUsingNoValuePropOnContextProvider = true; - console.error( - 'The `value` prop is required for the ``. Did you misspell it or forget to pass it?', - ); - } - } - const providerPropTypes = workInProgress.type.propTypes; - - if (providerPropTypes) { - checkPropTypes(providerPropTypes, newProps, 'prop', 'Context.Provider'); - } - } - - pushProvider(workInProgress, context, newValue); - - if (enableLazyContextPropagation) { - // In the lazy propagation implementation, we don't scan for matching - // consumers until something bails out, because until something bails out - // we're going to visit those nodes, anyway. The trade-off is that it shifts - // responsibility to the consumer to track whether something has changed. - } else { - if (oldProps !== null) { - const oldValue = oldProps.value; - if (is(oldValue, newValue)) { - // No change. Bailout early if children are the same. - if ( - oldProps.children === newProps.children && - !hasLegacyContextChanged() - ) { - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderLanes, - ); - } - } else { - // The context value changed. Search for matching consumers and schedule - // them to update. - propagateContextChange(workInProgress, context, renderLanes); - } - } - } - - const newChildren = newProps.children; - reconcileChildren(current, workInProgress, newChildren, renderLanes); - return workInProgress.child; -} - -let hasWarnedAboutUsingContextAsConsumer = false; - -function updateContextConsumer( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -) { - let context: ReactContext = workInProgress.type; - // The logic below for Context differs depending on PROD or DEV mode. In - // DEV mode, we create a separate object for Context.Consumer that acts - // like a proxy to Context. This proxy object adds unnecessary code in PROD - // so we use the old behaviour (Context.Consumer references Context) to - // reduce size and overhead. The separate object references context via - // a property called "_context", which also gives us the ability to check - // in DEV mode if this property exists or not and warn if it does not. - if (__DEV__) { - if ((context: any)._context === undefined) { - // This may be because it's a Context (rather than a Consumer). - // Or it may be because it's older React where they're the same thing. - // We only want to warn if we're sure it's a new React. - if (context !== context.Consumer) { - if (!hasWarnedAboutUsingContextAsConsumer) { - hasWarnedAboutUsingContextAsConsumer = true; - console.error( - 'Rendering directly is not supported and will be removed in ' + - 'a future major release. Did you mean to render instead?', - ); - } - } - } else { - context = (context: any)._context; - } - } - const newProps = workInProgress.pendingProps; - const render = newProps.children; - - if (__DEV__) { - if (typeof render !== 'function') { - console.error( - 'A context consumer was rendered with multiple children, or a child ' + - "that isn't a function. A context consumer expects a single child " + - 'that is a function. If you did pass a function, make sure there ' + - 'is no trailing or leading whitespace around it.', - ); - } - } - - prepareToReadContext(workInProgress, renderLanes); - const newValue = readContext(context); - if (enableSchedulingProfiler) { - markComponentRenderStarted(workInProgress); - } - let newChildren; - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - setIsRendering(true); - newChildren = render(newValue); - setIsRendering(false); - } else { - newChildren = render(newValue); - } - if (enableSchedulingProfiler) { - markComponentRenderStopped(); - } - - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - reconcileChildren(current, workInProgress, newChildren, renderLanes); - return workInProgress.child; -} - -function updateScopeComponent(current, workInProgress, renderLanes) { - const nextProps = workInProgress.pendingProps; - const nextChildren = nextProps.children; - - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - -export function markWorkInProgressReceivedUpdate() { - didReceiveUpdate = true; -} - -export function checkIfWorkInProgressReceivedUpdate(): boolean { - return didReceiveUpdate; -} - -function resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress) { - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - if (current !== null) { - // A lazy component only mounts if it suspended inside a non- - // concurrent tree, in an inconsistent state. We want to treat it like - // a new mount, even though an empty version of it already committed. - // Disconnect the alternate pointers. - current.alternate = null; - workInProgress.alternate = null; - // Since this is conceptually a new fiber, schedule a Placement effect - workInProgress.flags |= Placement; - } - } -} - -function bailoutOnAlreadyFinishedWork( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -): Fiber | null { - if (current !== null) { - // Reuse previous dependencies - workInProgress.dependencies = current.dependencies; - } - - if (enableProfilerTimer) { - // Don't update "base" render times for bailouts. - stopProfilerTimerIfRunning(workInProgress); - } - - markSkippedUpdateLanes(workInProgress.lanes); - - // Check if the children have any pending work. - if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { - // The children don't have any work either. We can skip them. - // TODO: Once we add back resuming, we should check if the children are - // a work-in-progress set. If so, we need to transfer their effects. - - if (enableLazyContextPropagation && current !== null) { - // Before bailing out, check if there are any context changes in - // the children. - lazilyPropagateParentContextChanges(current, workInProgress, renderLanes); - if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { - return null; - } - } else { - return null; - } - } - - // This fiber doesn't have work, but its subtree does. Clone the child - // fibers and continue. - cloneChildFibers(current, workInProgress); - return workInProgress.child; -} - -function remountFiber( - current: Fiber, - oldWorkInProgress: Fiber, - newWorkInProgress: Fiber, -): Fiber | null { - if (__DEV__) { - const returnFiber = oldWorkInProgress.return; - if (returnFiber === null) { - // eslint-disable-next-line react-internal/prod-error-codes - throw new Error('Cannot swap the root fiber.'); - } - - // Disconnect from the old current. - // It will get deleted. - current.alternate = null; - oldWorkInProgress.alternate = null; - - // Connect to the new tree. - newWorkInProgress.index = oldWorkInProgress.index; - newWorkInProgress.sibling = oldWorkInProgress.sibling; - newWorkInProgress.return = oldWorkInProgress.return; - newWorkInProgress.ref = oldWorkInProgress.ref; - - // Replace the child/sibling pointers above it. - if (oldWorkInProgress === returnFiber.child) { - returnFiber.child = newWorkInProgress; - } else { - let prevSibling = returnFiber.child; - if (prevSibling === null) { - // eslint-disable-next-line react-internal/prod-error-codes - throw new Error('Expected parent to have a child.'); - } - // $FlowFixMe[incompatible-use] found when upgrading Flow - while (prevSibling.sibling !== oldWorkInProgress) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - prevSibling = prevSibling.sibling; - if (prevSibling === null) { - // eslint-disable-next-line react-internal/prod-error-codes - throw new Error('Expected to find the previous sibling.'); - } - } - // $FlowFixMe[incompatible-use] found when upgrading Flow - prevSibling.sibling = newWorkInProgress; - } - - // Delete the old fiber and place the new one. - // Since the old fiber is disconnected, we have to schedule it manually. - const deletions = returnFiber.deletions; - if (deletions === null) { - returnFiber.deletions = [current]; - returnFiber.flags |= ChildDeletion; - } else { - deletions.push(current); - } - - newWorkInProgress.flags |= Placement; - - // Restart work from the new fiber. - return newWorkInProgress; - } else { - throw new Error( - 'Did not expect this call in production. ' + - 'This is a bug in React. Please file an issue.', - ); - } -} - -function checkScheduledUpdateOrContext( - current: Fiber, - renderLanes: Lanes, -): boolean { - // Before performing an early bailout, we must check if there are pending - // updates or context. - const updateLanes = current.lanes; - if (includesSomeLane(updateLanes, renderLanes)) { - return true; - } - // No pending update, but because context is propagated lazily, we need - // to check for a context change before we bail out. - if (enableLazyContextPropagation) { - const dependencies = current.dependencies; - if (dependencies !== null && checkIfContextChanged(dependencies)) { - return true; - } - } - return false; -} - -function attemptEarlyBailoutIfNoScheduledUpdate( - current: Fiber, - workInProgress: Fiber, - renderLanes: Lanes, -) { - // This fiber does not have any pending work. Bailout without entering - // the begin phase. There's still some bookkeeping we that needs to be done - // in this optimized path, mostly pushing stuff onto the stack. - switch (workInProgress.tag) { - case HostRoot: - pushHostRootContext(workInProgress); - const root: FiberRoot = workInProgress.stateNode; - pushRootTransition(workInProgress, root, renderLanes); - - if (enableTransitionTracing) { - pushRootMarkerInstance(workInProgress); - } - - if (enableCache) { - const cache: Cache = current.memoizedState.cache; - pushCacheProvider(workInProgress, cache); - } - resetHydrationState(); - break; - case HostResource: - case HostSingleton: - case HostComponent: - pushHostContext(workInProgress); - break; - case ClassComponent: { - const Component = workInProgress.type; - if (isLegacyContextProvider(Component)) { - pushLegacyContextProvider(workInProgress); - } - break; - } - case HostPortal: - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - break; - case ContextProvider: { - const newValue = workInProgress.memoizedProps.value; - const context: ReactContext = workInProgress.type._context; - pushProvider(workInProgress, context, newValue); - break; - } - case Profiler: - if (enableProfilerTimer) { - // Profiler should only call onRender when one of its descendants actually rendered. - const hasChildWork = includesSomeLane( - renderLanes, - workInProgress.childLanes, - ); - if (hasChildWork) { - workInProgress.flags |= Update; - } - - if (enableProfilerCommitHooks) { - // Reset effect durations for the next eventual effect phase. - // These are reset during render to allow the DevTools commit hook a chance to read them, - const stateNode = workInProgress.stateNode; - stateNode.effectDuration = 0; - stateNode.passiveEffectDuration = 0; - } - } - break; - case SuspenseComponent: { - const state: SuspenseState | null = workInProgress.memoizedState; - if (state !== null) { - if (state.dehydrated !== null) { - // We're not going to render the children, so this is just to maintain - // push/pop symmetry - pushPrimaryTreeSuspenseHandler(workInProgress); - // We know that this component will suspend again because if it has - // been unsuspended it has committed as a resolved Suspense component. - // If it needs to be retried, it should have work scheduled on it. - workInProgress.flags |= DidCapture; - // We should never render the children of a dehydrated boundary until we - // upgrade it. We return null instead of bailoutOnAlreadyFinishedWork. - return null; - } - - // If this boundary is currently timed out, we need to decide - // whether to retry the primary children, or to skip over it and - // go straight to the fallback. Check the priority of the primary - // child fragment. - const primaryChildFragment: Fiber = (workInProgress.child: any); - const primaryChildLanes = primaryChildFragment.childLanes; - if (includesSomeLane(renderLanes, primaryChildLanes)) { - // The primary children have pending work. Use the normal path - // to attempt to render the primary children again. - return updateSuspenseComponent(current, workInProgress, renderLanes); - } else { - // The primary child fragment does not have pending work marked - // on it - pushPrimaryTreeSuspenseHandler(workInProgress); - // The primary children do not have pending work with sufficient - // priority. Bailout. - const child = bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderLanes, - ); - if (child !== null) { - // The fallback children have pending work. Skip over the - // primary children and work on the fallback. - return child.sibling; - } else { - // Note: We can return `null` here because we already checked - // whether there were nested context consumers, via the call to - // `bailoutOnAlreadyFinishedWork` above. - return null; - } - } - } else { - pushPrimaryTreeSuspenseHandler(workInProgress); - } - break; - } - case SuspenseListComponent: { - const didSuspendBefore = (current.flags & DidCapture) !== NoFlags; - - let hasChildWork = includesSomeLane( - renderLanes, - workInProgress.childLanes, - ); - - if (enableLazyContextPropagation && !hasChildWork) { - // Context changes may not have been propagated yet. We need to do - // that now, before we can decide whether to bail out. - // TODO: We use `childLanes` as a heuristic for whether there is - // remaining work in a few places, including - // `bailoutOnAlreadyFinishedWork` and - // `updateDehydratedSuspenseComponent`. We should maybe extract this - // into a dedicated function. - lazilyPropagateParentContextChanges( - current, - workInProgress, - renderLanes, - ); - hasChildWork = includesSomeLane(renderLanes, workInProgress.childLanes); - } - - if (didSuspendBefore) { - if (hasChildWork) { - // If something was in fallback state last time, and we have all the - // same children then we're still in progressive loading state. - // Something might get unblocked by state updates or retries in the - // tree which will affect the tail. So we need to use the normal - // path to compute the correct tail. - return updateSuspenseListComponent( - current, - workInProgress, - renderLanes, - ); - } - // If none of the children had any work, that means that none of - // them got retried so they'll still be blocked in the same way - // as before. We can fast bail out. - workInProgress.flags |= DidCapture; - } - - // If nothing suspended before and we're rendering the same children, - // then the tail doesn't matter. Anything new that suspends will work - // in the "together" mode, so we can continue from the state we had. - const renderState = workInProgress.memoizedState; - if (renderState !== null) { - // Reset to the "together" mode in case we've started a different - // update in the past but didn't complete it. - renderState.rendering = null; - renderState.tail = null; - renderState.lastEffect = null; - } - pushSuspenseListContext(workInProgress, suspenseStackCursor.current); - - if (hasChildWork) { - break; - } else { - // If none of the children had any work, that means that none of - // them got retried so they'll still be blocked in the same way - // as before. We can fast bail out. - return null; - } - } - case OffscreenComponent: - case LegacyHiddenComponent: { - // Need to check if the tree still needs to be deferred. This is - // almost identical to the logic used in the normal update path, - // so we'll just enter that. The only difference is we'll bail out - // at the next level instead of this one, because the child props - // have not changed. Which is fine. - // TODO: Probably should refactor `beginWork` to split the bailout - // path from the normal path. I'm tempted to do a labeled break here - // but I won't :) - workInProgress.lanes = NoLanes; - return updateOffscreenComponent(current, workInProgress, renderLanes); - } - case CacheComponent: { - if (enableCache) { - const cache: Cache = current.memoizedState.cache; - pushCacheProvider(workInProgress, cache); - } - break; - } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - const instance: TracingMarkerInstance | null = workInProgress.stateNode; - if (instance !== null) { - pushMarkerInstance(workInProgress, instance); - } - } - } - } - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); -} - -function beginWork( - current: Fiber | null, - workInProgress: Fiber, - renderLanes: Lanes, -): Fiber | null { - if (__DEV__) { - if (workInProgress._debugNeedsRemount && current !== null) { - // This will restart the begin phase with a new fiber. - return remountFiber( - current, - workInProgress, - createFiberFromTypeAndProps( - workInProgress.type, - workInProgress.key, - workInProgress.pendingProps, - workInProgress._debugOwner || null, - workInProgress.mode, - workInProgress.lanes, - ), - ); - } - } - - if (current !== null) { - const oldProps = current.memoizedProps; - const newProps = workInProgress.pendingProps; - - if ( - oldProps !== newProps || - hasLegacyContextChanged() || - // Force a re-render if the implementation changed due to hot reload: - (__DEV__ ? workInProgress.type !== current.type : false) - ) { - // If props or context changed, mark the fiber as having performed work. - // This may be unset if the props are determined to be equal later (memo). - didReceiveUpdate = true; - } else { - // Neither props nor legacy context changes. Check if there's a pending - // update or context change. - const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( - current, - renderLanes, - ); - if ( - !hasScheduledUpdateOrContext && - // If this is the second pass of an error or suspense boundary, there - // may not be work scheduled on `current`, so we check for this flag. - (workInProgress.flags & DidCapture) === NoFlags - ) { - // No pending updates or context. Bail out now. - didReceiveUpdate = false; - return attemptEarlyBailoutIfNoScheduledUpdate( - current, - workInProgress, - renderLanes, - ); - } - if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { - // This is a special case that only exists for legacy mode. - // See https://github.com/facebook/react/pull/19216. - didReceiveUpdate = true; - } else { - // An update was scheduled on this fiber, but there are no new props - // nor legacy context. Set this to false. If an update queue or context - // consumer produces a changed value, it will set this to true. Otherwise, - // the component will assume the children have not changed and bail out. - didReceiveUpdate = false; - } - } - } else { - didReceiveUpdate = false; - - if (getIsHydrating() && isForkedChild(workInProgress)) { - // Check if this child belongs to a list of muliple children in - // its parent. - // - // In a true multi-threaded implementation, we would render children on - // parallel threads. This would represent the beginning of a new render - // thread for this subtree. - // - // We only use this for id generation during hydration, which is why the - // logic is located in this special branch. - const slotIndex = workInProgress.index; - const numberOfForks = getForksAtLevel(workInProgress); - pushTreeId(workInProgress, numberOfForks, slotIndex); - } - } - - // Before entering the begin phase, clear pending update priority. - // TODO: This assumes that we're about to evaluate the component and process - // the update queue. However, there's an exception: SimpleMemoComponent - // sometimes bails out later in the begin phase. This indicates that we should - // move this assignment out of the common path and into each branch. - workInProgress.lanes = NoLanes; - - switch (workInProgress.tag) { - case IndeterminateComponent: { - return mountIndeterminateComponent( - current, - workInProgress, - workInProgress.type, - renderLanes, - ); - } - case LazyComponent: { - const elementType = workInProgress.elementType; - return mountLazyComponent( - current, - workInProgress, - elementType, - renderLanes, - ); - } - case FunctionComponent: { - const Component = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - const resolvedProps = - workInProgress.elementType === Component - ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); - return updateFunctionComponent( - current, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - } - case ClassComponent: { - const Component = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - const resolvedProps = - workInProgress.elementType === Component - ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); - return updateClassComponent( - current, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - } - case HostRoot: - return updateHostRoot(current, workInProgress, renderLanes); - case HostResource: - if (enableFloat && supportsResources) { - return updateHostResource(current, workInProgress, renderLanes); - } - // eslint-disable-next-line no-fallthrough - case HostSingleton: - if (enableHostSingletons && supportsSingletons) { - return updateHostSingleton(current, workInProgress, renderLanes); - } - // eslint-disable-next-line no-fallthrough - case HostComponent: - return updateHostComponent(current, workInProgress, renderLanes); - case HostText: - return updateHostText(current, workInProgress); - case SuspenseComponent: - return updateSuspenseComponent(current, workInProgress, renderLanes); - case HostPortal: - return updatePortalComponent(current, workInProgress, renderLanes); - case ForwardRef: { - const type = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - const resolvedProps = - workInProgress.elementType === type - ? unresolvedProps - : resolveDefaultProps(type, unresolvedProps); - return updateForwardRef( - current, - workInProgress, - type, - resolvedProps, - renderLanes, - ); - } - case Fragment: - return updateFragment(current, workInProgress, renderLanes); - case Mode: - return updateMode(current, workInProgress, renderLanes); - case Profiler: - return updateProfiler(current, workInProgress, renderLanes); - case ContextProvider: - return updateContextProvider(current, workInProgress, renderLanes); - case ContextConsumer: - return updateContextConsumer(current, workInProgress, renderLanes); - case MemoComponent: { - const type = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - // Resolve outer props first, then resolve inner props. - let resolvedProps = resolveDefaultProps(type, unresolvedProps); - if (__DEV__) { - if (workInProgress.type !== workInProgress.elementType) { - const outerPropTypes = type.propTypes; - if (outerPropTypes) { - checkPropTypes( - outerPropTypes, - resolvedProps, // Resolved for outer only - 'prop', - getComponentNameFromType(type), - ); - } - } - } - resolvedProps = resolveDefaultProps(type.type, resolvedProps); - return updateMemoComponent( - current, - workInProgress, - type, - resolvedProps, - renderLanes, - ); - } - case SimpleMemoComponent: { - return updateSimpleMemoComponent( - current, - workInProgress, - workInProgress.type, - workInProgress.pendingProps, - renderLanes, - ); - } - case IncompleteClassComponent: { - const Component = workInProgress.type; - const unresolvedProps = workInProgress.pendingProps; - const resolvedProps = - workInProgress.elementType === Component - ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); - return mountIncompleteClassComponent( - current, - workInProgress, - Component, - resolvedProps, - renderLanes, - ); - } - case SuspenseListComponent: { - return updateSuspenseListComponent(current, workInProgress, renderLanes); - } - case ScopeComponent: { - if (enableScopeAPI) { - return updateScopeComponent(current, workInProgress, renderLanes); - } - break; - } - case OffscreenComponent: { - return updateOffscreenComponent(current, workInProgress, renderLanes); - } - case LegacyHiddenComponent: { - if (enableLegacyHidden) { - return updateLegacyHiddenComponent( - current, - workInProgress, - renderLanes, - ); - } - break; - } - case CacheComponent: { - if (enableCache) { - return updateCacheComponent(current, workInProgress, renderLanes); - } - break; - } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - return updateTracingMarkerComponent( - current, - workInProgress, - renderLanes, - ); - } - break; - } - } - - throw new Error( - `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` + - 'React. Please file an issue.', - ); -} - -export {beginWork}; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js deleted file mode 100644 index 889f8449066ed..0000000000000 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ /dev/null @@ -1,4469 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type { - Instance, - TextInstance, - SuspenseInstance, - Container, - ChildSet, - UpdatePayload, -} from './ReactFiberHostConfig'; -import type {Fiber, FiberRoot} from './ReactInternalTypes'; -import type {Lanes} from './ReactFiberLane.old'; -import {NoTimestamp, SyncLane} from './ReactFiberLane.old'; -import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; -import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; -import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; -import type {Wakeable} from 'shared/ReactTypes'; -import {isOffscreenManual} from './ReactFiberOffscreenComponent'; -import type { - OffscreenState, - OffscreenInstance, - OffscreenQueue, - OffscreenProps, -} from './ReactFiberOffscreenComponent'; -import type {HookFlags} from './ReactHookEffectTags'; -import type {Cache} from './ReactFiberCacheComponent.old'; -import type {RootState} from './ReactFiberRoot.old'; -import type { - Transition, - TracingMarkerInstance, - TransitionAbort, -} from './ReactFiberTracingMarkerComponent.old'; - -import { - enableCreateEventHandleAPI, - enableProfilerTimer, - enableProfilerCommitHooks, - enableProfilerNestedUpdatePhase, - enableSchedulingProfiler, - enableSuspenseCallback, - enableScopeAPI, - deletedTreeCleanUpLevel, - enableUpdaterTracking, - enableCache, - enableTransitionTracing, - enableUseEventHook, - enableFloat, - enableLegacyHidden, - enableHostSingletons, -} from 'shared/ReactFeatureFlags'; -import { - FunctionComponent, - ForwardRef, - ClassComponent, - HostRoot, - HostComponent, - HostResource, - HostSingleton, - HostText, - HostPortal, - Profiler, - SuspenseComponent, - DehydratedFragment, - IncompleteClassComponent, - MemoComponent, - SimpleMemoComponent, - SuspenseListComponent, - ScopeComponent, - OffscreenComponent, - LegacyHiddenComponent, - CacheComponent, - TracingMarkerComponent, -} from './ReactWorkTags'; -import { - NoFlags, - ContentReset, - Placement, - ChildDeletion, - Snapshot, - Update, - Callback, - Ref, - Hydrating, - Passive, - BeforeMutationMask, - MutationMask, - LayoutMask, - PassiveMask, - Visibility, -} from './ReactFiberFlags'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; -import { - resetCurrentFiber as resetCurrentDebugFiberInDEV, - setCurrentFiber as setCurrentDebugFiberInDEV, - getCurrentFiber as getCurrentDebugFiberInDEV, -} from './ReactCurrentFiber'; -import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; -import { - isCurrentUpdateNested, - getCommitTime, - recordLayoutEffectDuration, - startLayoutEffectTimer, - recordPassiveEffectDuration, - startPassiveEffectTimer, -} from './ReactProfilerTimer.old'; -import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; -import { - deferHiddenCallbacks, - commitHiddenCallbacks, - commitCallbacks, -} from './ReactFiberClassUpdateQueue.old'; -import { - getPublicInstance, - supportsMutation, - supportsPersistence, - supportsHydration, - supportsResources, - supportsSingletons, - commitMount, - commitUpdate, - resetTextContent, - commitTextUpdate, - appendChild, - appendChildToContainer, - insertBefore, - insertInContainerBefore, - removeChild, - removeChildFromContainer, - clearSuspenseBoundary, - clearSuspenseBoundaryFromContainer, - replaceContainerChildren, - createContainerChildSet, - hideInstance, - hideTextInstance, - unhideInstance, - unhideTextInstance, - commitHydratedContainer, - commitHydratedSuspenseInstance, - clearContainer, - prepareScopeUpdate, - prepareForCommit, - beforeActiveInstanceBlur, - detachDeletedInstance, - acquireResource, - releaseResource, - clearSingleton, - acquireSingletonInstance, - releaseSingletonInstance, -} from './ReactFiberHostConfig'; -import { - captureCommitPhaseError, - resolveRetryWakeable, - markCommitTimeOfFallback, - enqueuePendingPassiveProfilerEffect, - restorePendingUpdaters, - addTransitionStartCallbackToPendingTransition, - addTransitionProgressCallbackToPendingTransition, - addTransitionCompleteCallbackToPendingTransition, - addMarkerProgressCallbackToPendingTransition, - addMarkerIncompleteCallbackToPendingTransition, - addMarkerCompleteCallbackToPendingTransition, - setIsRunningInsertionEffect, - getExecutionContext, - CommitContext, - NoContext, -} from './ReactFiberWorkLoop.old'; -import { - NoFlags as NoHookEffect, - HasEffect as HookHasEffect, - Layout as HookLayout, - Insertion as HookInsertion, - Passive as HookPassive, -} from './ReactHookEffectTags'; -import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old'; -import {doesFiberContain} from './ReactFiberTreeReflection'; -import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils'; -import { - isDevToolsPresent, - markComponentPassiveEffectMountStarted, - markComponentPassiveEffectMountStopped, - markComponentPassiveEffectUnmountStarted, - markComponentPassiveEffectUnmountStopped, - markComponentLayoutEffectMountStarted, - markComponentLayoutEffectMountStopped, - markComponentLayoutEffectUnmountStarted, - markComponentLayoutEffectUnmountStopped, - onCommitUnmount, -} from './ReactFiberDevToolsHook.old'; -import {releaseCache, retainCache} from './ReactFiberCacheComponent.old'; -import {clearTransitionsForLanes} from './ReactFiberLane.old'; -import { - OffscreenVisible, - OffscreenDetached, - OffscreenPassiveEffectsConnected, -} from './ReactFiberOffscreenComponent'; -import { - TransitionRoot, - TransitionTracingMarker, -} from './ReactFiberTracingMarkerComponent.old'; -import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop.old'; -import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; - -let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; -if (__DEV__) { - didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); -} - -// Used during the commit phase to track the state of the Offscreen component stack. -// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor. -let offscreenSubtreeIsHidden: boolean = false; -let offscreenSubtreeWasHidden: boolean = false; - -const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; - -let nextEffect: Fiber | null = null; - -// Used for Profiling builds to track updaters. -let inProgressLanes: Lanes | null = null; -let inProgressRoot: FiberRoot | null = null; - -function shouldProfile(current: Fiber): boolean { - return ( - enableProfilerTimer && - enableProfilerCommitHooks && - (current.mode & ProfileMode) !== NoMode && - (getExecutionContext() & CommitContext) !== NoContext - ); -} - -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 - // (https://github.com/facebook/react/issues/21712). - // As a compromise, rethrow only caught errors in a guard. - if (__DEV__) { - invokeGuardedCallback(null, () => { - throw error; - }); - clearCaughtError(); - } -} - -const callComponentWillUnmountWithTimer = function(current, instance) { - instance.props = current.memoizedProps; - instance.state = current.memoizedState; - if (shouldProfile(current)) { - try { - startLayoutEffectTimer(); - instance.componentWillUnmount(); - } finally { - recordLayoutEffectDuration(current); - } - } else { - instance.componentWillUnmount(); - } -}; - -// Capture errors so they don't interrupt unmounting. -function safelyCallComponentWillUnmount( - current: Fiber, - nearestMountedAncestor: Fiber | null, - instance: any, -) { - try { - callComponentWillUnmountWithTimer(current, instance); - } catch (error) { - captureCommitPhaseError(current, nearestMountedAncestor, error); - } -} - -// Capture errors so they don't interrupt mounting. -function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { - try { - commitAttachRef(current); - } catch (error) { - captureCommitPhaseError(current, nearestMountedAncestor, error); - } -} - -function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) { - const ref = current.ref; - const refCleanup = current.refCleanup; - - if (ref !== null) { - if (typeof refCleanup === 'function') { - try { - if (shouldProfile(current)) { - try { - startLayoutEffectTimer(); - refCleanup(); - } finally { - recordLayoutEffectDuration(current); - } - } else { - refCleanup(); - } - } catch (error) { - captureCommitPhaseError(current, nearestMountedAncestor, error); - } finally { - // `refCleanup` has been called. Nullify all references to it to prevent double invocation. - current.refCleanup = null; - const finishedWork = current.alternate; - if (finishedWork != null) { - finishedWork.refCleanup = null; - } - } - } else if (typeof ref === 'function') { - let retVal; - try { - if (shouldProfile(current)) { - try { - startLayoutEffectTimer(); - retVal = ref(null); - } finally { - recordLayoutEffectDuration(current); - } - } else { - retVal = ref(null); - } - } catch (error) { - captureCommitPhaseError(current, nearestMountedAncestor, error); - } - if (__DEV__) { - if (typeof retVal === 'function') { - console.error( - 'Unexpected return value from a callback ref in %s. ' + - 'A callback ref should not return a function.', - getComponentNameFromFiber(current), - ); - } - } - } else { - // $FlowFixMe unable to narrow type to RefObject - ref.current = null; - } - } -} - -function safelyCallDestroy( - current: Fiber, - nearestMountedAncestor: Fiber | null, - destroy: () => void, -) { - try { - destroy(); - } catch (error) { - captureCommitPhaseError(current, nearestMountedAncestor, error); - } -} - -let focusedInstanceHandle: null | Fiber = null; -let shouldFireAfterActiveInstanceBlur: boolean = false; - -export function commitBeforeMutationEffects( - root: FiberRoot, - firstChild: Fiber, -): boolean { - focusedInstanceHandle = prepareForCommit(root.containerInfo); - - nextEffect = firstChild; - commitBeforeMutationEffects_begin(); - - // We no longer need to track the active instance fiber - const shouldFire = shouldFireAfterActiveInstanceBlur; - shouldFireAfterActiveInstanceBlur = false; - focusedInstanceHandle = null; - - return shouldFire; -} - -function commitBeforeMutationEffects_begin() { - while (nextEffect !== null) { - const fiber = nextEffect; - - // This phase is only used for beforeActiveInstanceBlur. - // Let's skip the whole loop if it's off. - if (enableCreateEventHandleAPI) { - // 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 deletion = deletions[i]; - commitBeforeMutationEffectsDeletion(deletion); - } - } - } - - const child = fiber.child; - if ( - (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && - child !== null - ) { - child.return = fiber; - nextEffect = child; - } else { - commitBeforeMutationEffects_complete(); - } - } -} - -function commitBeforeMutationEffects_complete() { - while (nextEffect !== null) { - const fiber = nextEffect; - setCurrentDebugFiberInDEV(fiber); - try { - commitBeforeMutationEffectsOnFiber(fiber); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - - const sibling = fiber.sibling; - if (sibling !== null) { - sibling.return = fiber.return; - nextEffect = sibling; - return; - } - - nextEffect = fiber.return; - } -} - -function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { - const current = finishedWork.alternate; - const flags = finishedWork.flags; - - if (enableCreateEventHandleAPI) { - if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { - // Check to see if the focused element was inside of a hidden (Suspense) subtree. - // TODO: Move this out of the hot path using a dedicated effect tag. - if ( - finishedWork.tag === SuspenseComponent && - isSuspenseBoundaryBeingHidden(current, finishedWork) && - // $FlowFixMe[incompatible-call] found when upgrading Flow - doesFiberContain(finishedWork, focusedInstanceHandle) - ) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(finishedWork); - } - } - } - - if ((flags & Snapshot) !== NoFlags) { - setCurrentDebugFiberInDEV(finishedWork); - } - - switch (finishedWork.tag) { - case FunctionComponent: { - if (enableUseEventHook) { - if ((flags & Update) !== NoFlags) { - commitUseEventMount(finishedWork); - } - } - break; - } - case ForwardRef: - case SimpleMemoComponent: { - break; - } - case ClassComponent: { - if ((flags & Snapshot) !== NoFlags) { - if (current !== null) { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - const instance = finishedWork.stateNode; - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'getSnapshotBeforeUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'getSnapshotBeforeUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - } - } - const snapshot = instance.getSnapshotBeforeUpdate( - finishedWork.elementType === finishedWork.type - ? prevProps - : resolveDefaultProps(finishedWork.type, prevProps), - prevState, - ); - if (__DEV__) { - const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); - if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { - didWarnSet.add(finishedWork.type); - console.error( - '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + - 'must be returned. You have returned undefined.', - getComponentNameFromFiber(finishedWork), - ); - } - } - instance.__reactInternalSnapshotBeforeUpdate = snapshot; - } - } - break; - } - case HostRoot: { - if ((flags & Snapshot) !== NoFlags) { - if (supportsMutation) { - const root = finishedWork.stateNode; - clearContainer(root.containerInfo); - } - } - break; - } - case HostComponent: - case HostResource: - case HostSingleton: - case HostText: - case HostPortal: - case IncompleteClassComponent: - // Nothing to do for these component types - break; - default: { - if ((flags & Snapshot) !== NoFlags) { - 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.', - ); - } - } - } - - if ((flags & Snapshot) !== NoFlags) { - resetCurrentDebugFiberInDEV(); - } -} - -function commitBeforeMutationEffectsDeletion(deletion: Fiber) { - if (enableCreateEventHandleAPI) { - // TODO (effects) It would be nice to avoid calling doesFiberContain() - // Maybe we can repurpose one of the subtreeFlags positions for this instead? - // Use it to store which part of the tree the focused instance is in? - // This assumes we can safely determine that instance during the "render" phase. - if (doesFiberContain(deletion, ((focusedInstanceHandle: any): Fiber))) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(deletion); - } - } -} - -function commitHookEffectListUnmount( - flags: HookFlags, - finishedWork: Fiber, - nearestMountedAncestor: Fiber | null, -) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - if ((effect.tag & flags) === flags) { - // Unmount - const destroy = effect.destroy; - effect.destroy = undefined; - if (destroy !== undefined) { - if (enableSchedulingProfiler) { - if ((flags & HookPassive) !== NoHookEffect) { - markComponentPassiveEffectUnmountStarted(finishedWork); - } else if ((flags & HookLayout) !== NoHookEffect) { - markComponentLayoutEffectUnmountStarted(finishedWork); - } - } - - if (__DEV__) { - if ((flags & HookInsertion) !== NoHookEffect) { - setIsRunningInsertionEffect(true); - } - } - safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); - if (__DEV__) { - if ((flags & HookInsertion) !== NoHookEffect) { - setIsRunningInsertionEffect(false); - } - } - - if (enableSchedulingProfiler) { - if ((flags & HookPassive) !== NoHookEffect) { - markComponentPassiveEffectUnmountStopped(); - } else if ((flags & HookLayout) !== NoHookEffect) { - markComponentLayoutEffectUnmountStopped(); - } - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } -} - -function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - if ((effect.tag & flags) === flags) { - if (enableSchedulingProfiler) { - if ((flags & HookPassive) !== NoHookEffect) { - markComponentPassiveEffectMountStarted(finishedWork); - } else if ((flags & HookLayout) !== NoHookEffect) { - markComponentLayoutEffectMountStarted(finishedWork); - } - } - - // Mount - const create = effect.create; - if (__DEV__) { - if ((flags & HookInsertion) !== NoHookEffect) { - setIsRunningInsertionEffect(true); - } - } - effect.destroy = create(); - if (__DEV__) { - if ((flags & HookInsertion) !== NoHookEffect) { - setIsRunningInsertionEffect(false); - } - } - - if (enableSchedulingProfiler) { - if ((flags & HookPassive) !== NoHookEffect) { - markComponentPassiveEffectMountStopped(); - } else if ((flags & HookLayout) !== NoHookEffect) { - markComponentLayoutEffectMountStopped(); - } - } - - if (__DEV__) { - const destroy = effect.destroy; - if (destroy !== undefined && typeof destroy !== 'function') { - let hookName; - if ((effect.tag & HookLayout) !== NoFlags) { - hookName = 'useLayoutEffect'; - } else if ((effect.tag & HookInsertion) !== NoFlags) { - hookName = 'useInsertionEffect'; - } else { - hookName = 'useEffect'; - } - let addendum; - if (destroy === null) { - addendum = - ' You returned null. If your effect does not require clean ' + - 'up, return undefined (or nothing).'; - } else if (typeof destroy.then === 'function') { - addendum = - '\n\nIt looks like you wrote ' + - hookName + - '(async () => ...) or returned a Promise. ' + - 'Instead, write the async function inside your effect ' + - 'and call it immediately:\n\n' + - hookName + - '(() => {\n' + - ' async function fetchData() {\n' + - ' // You can await here\n' + - ' const response = await MyAPI.getData(someId);\n' + - ' // ...\n' + - ' }\n' + - ' fetchData();\n' + - `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + - 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'; - } else { - addendum = ' You returned: ' + destroy; - } - console.error( - '%s must not return anything besides a function, ' + - 'which is used for clean-up.%s', - hookName, - addendum, - ); - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } -} - -function commitUseEventMount(finishedWork: Fiber) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const eventPayloads = updateQueue !== null ? updateQueue.events : null; - if (eventPayloads !== null) { - for (let ii = 0; ii < eventPayloads.length; ii++) { - const {ref, nextImpl} = eventPayloads[ii]; - ref.impl = nextImpl; - } - } -} - -export function commitPassiveEffectDurations( - finishedRoot: FiberRoot, - finishedWork: Fiber, -): void { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - getExecutionContext() & CommitContext - ) { - // Only Profilers with work in their subtree will have an Update effect scheduled. - if ((finishedWork.flags & Update) !== NoFlags) { - switch (finishedWork.tag) { - case Profiler: { - const {passiveEffectDuration} = finishedWork.stateNode; - const {id, onPostCommit} = finishedWork.memoizedProps; - - // This value will still reflect the previous commit phase. - // It does not get reset until the start of the next commit phase. - const commitTime = getCommitTime(); - - let phase = finishedWork.alternate === null ? 'mount' : 'update'; - if (enableProfilerNestedUpdatePhase) { - if (isCurrentUpdateNested()) { - phase = 'nested-update'; - } - } - - if (typeof onPostCommit === 'function') { - onPostCommit(id, phase, passiveEffectDuration, commitTime); - } - - // Bubble times to the next nearest ancestor Profiler. - // After we process that Profiler, we'll bubble further up. - let parentFiber = finishedWork.return; - outer: while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.passiveEffectDuration += passiveEffectDuration; - break outer; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.passiveEffectDuration += passiveEffectDuration; - break outer; - } - parentFiber = parentFiber.return; - } - break; - } - default: - break; - } - } - } -} - -function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) { - // At this point layout effects have already been destroyed (during mutation phase). - // This is done to prevent sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - if (shouldProfile(finishedWork)) { - try { - startLayoutEffectTimer(); - commitHookEffectListMount(hookFlags, finishedWork); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - recordLayoutEffectDuration(finishedWork); - } else { - try { - commitHookEffectListMount(hookFlags, finishedWork); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } -} - -function commitClassLayoutLifecycles( - finishedWork: Fiber, - current: Fiber | null, -) { - const instance = finishedWork.stateNode; - if (current === null) { - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'componentDidMount. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'componentDidMount. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - } - } - if (shouldProfile(finishedWork)) { - try { - startLayoutEffectTimer(); - instance.componentDidMount(); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - recordLayoutEffectDuration(finishedWork); - } else { - try { - instance.componentDidMount(); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } - } else { - const prevProps = - finishedWork.elementType === finishedWork.type - ? current.memoizedProps - : resolveDefaultProps(finishedWork.type, current.memoizedProps); - const prevState = current.memoizedState; - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'componentDidUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'componentDidUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - } - } - if (shouldProfile(finishedWork)) { - try { - startLayoutEffectTimer(); - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, - ); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - recordLayoutEffectDuration(finishedWork); - } else { - try { - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, - ); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } - } -} - -function commitClassCallbacks(finishedWork: Fiber) { - // TODO: I think this is now always non-null by the time it reaches the - // commit phase. Consider removing the type check. - const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - const instance = finishedWork.stateNode; - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'processing the update queue. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'processing the update queue. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentNameFromFiber(finishedWork) || 'instance', - ); - } - } - } - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - try { - commitCallbacks(updateQueue, instance); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } -} - -function commitHostComponentMount(finishedWork: Fiber) { - const type = finishedWork.type; - const props = finishedWork.memoizedProps; - const instance: Instance = finishedWork.stateNode; - try { - commitMount(instance, type, props, finishedWork); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } -} - -function commitProfilerUpdate(finishedWork: Fiber, current: Fiber | null) { - if (enableProfilerTimer && getExecutionContext() & CommitContext) { - try { - const {onCommit, onRender} = finishedWork.memoizedProps; - const {effectDuration} = finishedWork.stateNode; - - const commitTime = getCommitTime(); - - let phase = current === null ? 'mount' : 'update'; - if (enableProfilerNestedUpdatePhase) { - if (isCurrentUpdateNested()) { - phase = 'nested-update'; - } - } - - if (typeof onRender === 'function') { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - ); - } - - if (enableProfilerCommitHooks) { - if (typeof onCommit === 'function') { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - ); - } - - // Schedule a passive effect for this Profiler to call onPostCommit hooks. - // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, - // because the effect is also where times bubble to parent Profilers. - enqueuePendingPassiveProfilerEffect(finishedWork); - - // Propagate layout effect durations to the next nearest Profiler ancestor. - // Do not reset these values until the next render so DevTools has a chance to read them first. - let parentFiber = finishedWork.return; - outer: while (parentFiber !== null) { - switch (parentFiber.tag) { - case HostRoot: - const root = parentFiber.stateNode; - root.effectDuration += effectDuration; - break outer; - case Profiler: - const parentStateNode = parentFiber.stateNode; - parentStateNode.effectDuration += effectDuration; - break outer; - } - parentFiber = parentFiber.return; - } - } - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } -} - -function commitLayoutEffectOnFiber( - finishedRoot: FiberRoot, - current: Fiber | null, - finishedWork: Fiber, - committedLanes: Lanes, -): void { - // When updating this function, also update reappearLayoutEffects, which does - // most of the same things when an offscreen tree goes from hidden -> visible. - const flags = finishedWork.flags; - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - if (flags & Update) { - commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect); - } - break; - } - case ClassComponent: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - if (flags & Update) { - commitClassLayoutLifecycles(finishedWork, current); - } - - if (flags & Callback) { - commitClassCallbacks(finishedWork); - } - - if (flags & Ref) { - safelyAttachRef(finishedWork, finishedWork.return); - } - break; - } - case HostRoot: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - if (flags & Callback) { - // TODO: I think this is now always non-null by the time it reaches the - // commit phase. Consider removing the type check. - const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - let instance = null; - if (finishedWork.child !== null) { - switch (finishedWork.child.tag) { - case HostSingleton: - case HostComponent: - instance = getPublicInstance(finishedWork.child.stateNode); - break; - case ClassComponent: - instance = finishedWork.child.stateNode; - break; - } - } - try { - commitCallbacks(updateQueue, instance); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } - } - break; - } - case HostResource: { - if (enableFloat && supportsResources) { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - - if (flags & Ref) { - safelyAttachRef(finishedWork, finishedWork.return); - } - break; - } - } - // eslint-disable-next-line-no-fallthrough - case HostSingleton: - case HostComponent: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - - // Renderers may schedule work to be done after host components are mounted - // (eg DOM renderer may schedule auto-focus for inputs and form controls). - // These effects should only be committed when components are first mounted, - // aka when there is no current/alternate. - if (current === null && flags & Update) { - commitHostComponentMount(finishedWork); - } - - if (flags & Ref) { - safelyAttachRef(finishedWork, finishedWork.return); - } - break; - } - case Profiler: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - // TODO: Should this fire inside an offscreen tree? Or should it wait to - // fire when the tree becomes visible again. - if (flags & Update) { - commitProfilerUpdate(finishedWork, current); - } - break; - } - case SuspenseComponent: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - if (flags & Update) { - commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); - } - break; - } - case OffscreenComponent: { - const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode; - if (isModernRoot) { - const isHidden = finishedWork.memoizedState !== null; - const newOffscreenSubtreeIsHidden = - isHidden || offscreenSubtreeIsHidden; - if (newOffscreenSubtreeIsHidden) { - // The Offscreen tree is hidden. Skip over its layout effects. - } else { - // The Offscreen tree is visible. - - const wasHidden = current !== null && current.memoizedState !== null; - const newOffscreenSubtreeWasHidden = - wasHidden || offscreenSubtreeWasHidden; - const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden; - const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; - offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden; - offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden; - - if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) { - // This is the root of a reappearing boundary. As we continue - // traversing the layout effects, we must also re-mount layout - // effects that were unmounted when the Offscreen subtree was - // hidden. So this is a superset of the normal commitLayoutEffects. - const includeWorkInProgressEffects = - (finishedWork.subtreeFlags & LayoutMask) !== NoFlags; - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - } else { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - } - offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden; - offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; - } - } else { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - } - if (flags & Ref) { - const props: OffscreenProps = finishedWork.memoizedProps; - if (props.mode === 'manual') { - safelyAttachRef(finishedWork, finishedWork.return); - } else { - safelyDetachRef(finishedWork, finishedWork.return); - } - } - break; - } - default: { - recursivelyTraverseLayoutEffects( - finishedRoot, - finishedWork, - committedLanes, - ); - break; - } - } -} - -function abortRootTransitions( - root: FiberRoot, - abort: TransitionAbort, - deletedTransitions: Set, - deletedOffscreenInstance: OffscreenInstance | null, - isInDeletedTree: boolean, -) { - if (enableTransitionTracing) { - const rootTransitions = root.incompleteTransitions; - deletedTransitions.forEach(transition => { - if (rootTransitions.has(transition)) { - const transitionInstance: TracingMarkerInstance = (rootTransitions.get( - transition, - ): any); - if (transitionInstance.aborts === null) { - transitionInstance.aborts = []; - } - transitionInstance.aborts.push(abort); - - if (deletedOffscreenInstance !== null) { - if ( - transitionInstance.pendingBoundaries !== null && - transitionInstance.pendingBoundaries.has(deletedOffscreenInstance) - ) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - transitionInstance.pendingBoundaries.delete( - deletedOffscreenInstance, - ); - } - } - } - }); - } -} - -function abortTracingMarkerTransitions( - abortedFiber: Fiber, - abort: TransitionAbort, - deletedTransitions: Set, - deletedOffscreenInstance: OffscreenInstance | null, - isInDeletedTree: boolean, -) { - if (enableTransitionTracing) { - const markerInstance: TracingMarkerInstance = abortedFiber.stateNode; - const markerTransitions = markerInstance.transitions; - const pendingBoundaries = markerInstance.pendingBoundaries; - if (markerTransitions !== null) { - // TODO: Refactor this code. Is there a way to move this code to - // the deletions phase instead of calculating it here while making sure - // complete is called appropriately? - deletedTransitions.forEach(transition => { - // If one of the transitions on the tracing marker is a transition - // that was in an aborted subtree, we will abort that tracing marker - if ( - abortedFiber !== null && - markerTransitions.has(transition) && - (markerInstance.aborts === null || - !markerInstance.aborts.includes(abort)) - ) { - if (markerInstance.transitions !== null) { - if (markerInstance.aborts === null) { - markerInstance.aborts = [abort]; - addMarkerIncompleteCallbackToPendingTransition( - abortedFiber.memoizedProps.name, - markerInstance.transitions, - markerInstance.aborts, - ); - } else { - markerInstance.aborts.push(abort); - } - - // We only want to call onTransitionProgress when the marker hasn't been - // deleted - if ( - deletedOffscreenInstance !== null && - !isInDeletedTree && - pendingBoundaries !== null && - pendingBoundaries.has(deletedOffscreenInstance) - ) { - pendingBoundaries.delete(deletedOffscreenInstance); - - addMarkerProgressCallbackToPendingTransition( - abortedFiber.memoizedProps.name, - deletedTransitions, - pendingBoundaries, - ); - } - } - } - }); - } - } -} - -function abortParentMarkerTransitionsForDeletedFiber( - abortedFiber: Fiber, - abort: TransitionAbort, - deletedTransitions: Set, - deletedOffscreenInstance: OffscreenInstance | null, - isInDeletedTree: boolean, -) { - if (enableTransitionTracing) { - // Find all pending markers that are waiting on child suspense boundaries in the - // aborted subtree and cancels them - let fiber: null | Fiber = abortedFiber; - while (fiber !== null) { - switch (fiber.tag) { - case TracingMarkerComponent: - abortTracingMarkerTransitions( - fiber, - abort, - deletedTransitions, - deletedOffscreenInstance, - isInDeletedTree, - ); - break; - case HostRoot: - const root = fiber.stateNode; - abortRootTransitions( - root, - abort, - deletedTransitions, - deletedOffscreenInstance, - isInDeletedTree, - ); - - break; - default: - break; - } - - fiber = fiber.return; - } - } -} - -function commitTransitionProgress(offscreenFiber: Fiber) { - if (enableTransitionTracing) { - // This function adds suspense boundaries to the root - // or tracing marker's pendingBoundaries map. - // When a suspense boundary goes from a resolved to a fallback - // state we add the boundary to the map, and when it goes from - // a fallback to a resolved state, we remove the boundary from - // the map. - - // We use stateNode on the Offscreen component as a stable object - // that doesnt change from render to render. This way we can - // distinguish between different Offscreen instances (vs. the same - // Offscreen instance with different fibers) - const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode; - - let prevState: SuspenseState | null = null; - const previousFiber = offscreenFiber.alternate; - if (previousFiber !== null && previousFiber.memoizedState !== null) { - prevState = previousFiber.memoizedState; - } - const nextState: SuspenseState | null = offscreenFiber.memoizedState; - - const wasHidden = prevState !== null; - const isHidden = nextState !== null; - - const pendingMarkers = offscreenInstance._pendingMarkers; - // If there is a name on the suspense boundary, store that in - // the pending boundaries. - let name = null; - const parent = offscreenFiber.return; - if ( - parent !== null && - parent.tag === SuspenseComponent && - parent.memoizedProps.unstable_name - ) { - name = parent.memoizedProps.unstable_name; - } - - if (!wasHidden && isHidden) { - // The suspense boundaries was just hidden. Add the boundary - // to the pending boundary set if it's there - if (pendingMarkers !== null) { - pendingMarkers.forEach(markerInstance => { - const pendingBoundaries = markerInstance.pendingBoundaries; - const transitions = markerInstance.transitions; - const markerName = markerInstance.name; - if ( - pendingBoundaries !== null && - !pendingBoundaries.has(offscreenInstance) - ) { - pendingBoundaries.set(offscreenInstance, { - name, - }); - if (transitions !== null) { - if ( - markerInstance.tag === TransitionTracingMarker && - markerName !== null - ) { - addMarkerProgressCallbackToPendingTransition( - markerName, - transitions, - pendingBoundaries, - ); - } else if (markerInstance.tag === TransitionRoot) { - transitions.forEach(transition => { - addTransitionProgressCallbackToPendingTransition( - transition, - pendingBoundaries, - ); - }); - } - } - } - }); - } - } else if (wasHidden && !isHidden) { - // The suspense boundary went from hidden to visible. Remove - // the boundary from the pending suspense boundaries set - // if it's there - if (pendingMarkers !== null) { - pendingMarkers.forEach(markerInstance => { - const pendingBoundaries = markerInstance.pendingBoundaries; - const transitions = markerInstance.transitions; - const markerName = markerInstance.name; - if ( - pendingBoundaries !== null && - pendingBoundaries.has(offscreenInstance) - ) { - pendingBoundaries.delete(offscreenInstance); - if (transitions !== null) { - if ( - markerInstance.tag === TransitionTracingMarker && - markerName !== null - ) { - addMarkerProgressCallbackToPendingTransition( - markerName, - transitions, - pendingBoundaries, - ); - - // If there are no more unresolved suspense boundaries, the interaction - // is considered finished - if (pendingBoundaries.size === 0) { - if (markerInstance.aborts === null) { - addMarkerCompleteCallbackToPendingTransition( - markerName, - transitions, - ); - } - markerInstance.transitions = null; - markerInstance.pendingBoundaries = null; - markerInstance.aborts = null; - } - } else if (markerInstance.tag === TransitionRoot) { - transitions.forEach(transition => { - addTransitionProgressCallbackToPendingTransition( - transition, - pendingBoundaries, - ); - }); - } - } - } - }); - } - } - } -} - -function hideOrUnhideAllChildren(finishedWork, isHidden) { - // Only hide or unhide the top-most host nodes. - let hostSubtreeRoot = null; - - if (supportsMutation) { - // We only have the top Fiber that was inserted but we need to recurse down its - // children to find all the terminal nodes. - let node: Fiber = finishedWork; - while (true) { - if ( - node.tag === HostComponent || - (enableFloat && supportsResources - ? node.tag === HostResource - : false) || - (enableHostSingletons && supportsSingletons - ? node.tag === HostSingleton - : false) - ) { - if (hostSubtreeRoot === null) { - hostSubtreeRoot = node; - 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) { - try { - const instance = node.stateNode; - if (isHidden) { - hideTextInstance(instance); - } else { - unhideTextInstance(instance, node.memoizedProps); - } - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } - } else if ( - (node.tag === OffscreenComponent || - node.tag === LegacyHiddenComponent) && - (node.memoizedState: OffscreenState) !== null && - node !== finishedWork - ) { - // Found a nested Offscreen component that is hidden. - // Don't search any deeper. This tree should remain hidden. - } else if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - - if (node === finishedWork) { - return; - } - while (node.sibling === null) { - if (node.return === null || node.return === finishedWork) { - return; - } - - if (hostSubtreeRoot === node) { - hostSubtreeRoot = null; - } - - node = node.return; - } - - if (hostSubtreeRoot === node) { - hostSubtreeRoot = null; - } - - node.sibling.return = node.return; - node = node.sibling; - } - } -} - -function commitAttachRef(finishedWork: Fiber) { - const ref = finishedWork.ref; - if (ref !== null) { - const instance = finishedWork.stateNode; - let instanceToUse; - switch (finishedWork.tag) { - case HostResource: - case HostSingleton: - case HostComponent: - instanceToUse = getPublicInstance(instance); - break; - default: - instanceToUse = instance; - } - // Moved outside to ensure DCE works with this flag - if (enableScopeAPI && finishedWork.tag === ScopeComponent) { - instanceToUse = instance; - } - if (typeof ref === 'function') { - if (shouldProfile(finishedWork)) { - try { - startLayoutEffectTimer(); - finishedWork.refCleanup = ref(instanceToUse); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - finishedWork.refCleanup = ref(instanceToUse); - } - } else { - if (__DEV__) { - if (!ref.hasOwnProperty('current')) { - console.error( - 'Unexpected ref object provided for %s. ' + - 'Use either a ref-setter function or React.createRef().', - getComponentNameFromFiber(finishedWork), - ); - } - } - - // $FlowFixMe unable to narrow type to the non-function case - ref.current = instanceToUse; - } - } -} - -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. - // It also prevents events from bubbling from within disconnected components. - // - // Ideally, we should also clear the child pointer of the parent alternate to let this - // get GC:ed but we don't know which for sure which parent is the current - // one so we'll settle for GC:ing the subtree of this child. - // This child itself will be GC:ed when the parent updates the next time. - // - // Note that we can't clear child or sibling pointers yet. - // They're needed for passive effects and for findDOMNode. - // We defer those fields, and all other cleanup, to the passive phase (see detachFiberAfterEffects). - // - // Don't reset the alternate yet, either. We need that so we can detach the - // alternate's fields in the passive phase. Clearing the return pointer is - // sufficient for findDOMNode semantics. - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.return = null; - } - fiber.return = null; -} - -function detachFiberAfterEffects(fiber: Fiber) { - const alternate = fiber.alternate; - if (alternate !== null) { - fiber.alternate = null; - detachFiberAfterEffects(alternate); - } - - // Note: Defensively using negation instead of < in case - // `deletedTreeCleanUpLevel` is undefined. - if (!(deletedTreeCleanUpLevel >= 2)) { - // This is the default branch (level 0). - fiber.child = null; - fiber.deletions = null; - fiber.dependencies = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.sibling = null; - fiber.stateNode = null; - fiber.updateQueue = null; - - if (__DEV__) { - fiber._debugOwner = null; - } - } else { - // Clear cyclical Fiber fields. This level alone is designed to roughly - // approximate the planned Fiber refactor. In that world, `setState` will be - // bound to a special "instance" object instead of a Fiber. The Instance - // object will not have any of these fields. It will only be connected to - // the fiber tree via a single link at the root. So if this level alone is - // sufficient to fix memory issues, that bodes well for our plans. - fiber.child = null; - fiber.deletions = null; - fiber.sibling = null; - - // The `stateNode` is cyclical because on host nodes it points to the host - // tree, which has its own pointers to children, parents, and siblings. - // The other host nodes also point back to fibers, so we should detach that - // one, too. - if (fiber.tag === HostComponent) { - const hostInstance: Instance = fiber.stateNode; - if (hostInstance !== null) { - detachDeletedInstance(hostInstance); - } - } - fiber.stateNode = null; - - // I'm intentionally not clearing the `return` field in this level. We - // already disconnect the `return` pointer at the root of the deleted - // subtree (in `detachFiberMutation`). Besides, `return` by itself is not - // cyclical — it's only cyclical when combined with `child`, `sibling`, and - // `alternate`. But we'll clear it in the next level anyway, just in case. - - if (__DEV__) { - fiber._debugOwner = null; - } - - if (deletedTreeCleanUpLevel >= 3) { - // Theoretically, nothing in here should be necessary, because we already - // disconnected the fiber from the tree. So even if something leaks this - // particular fiber, it won't leak anything else - // - // The purpose of this branch is to be super aggressive so we can measure - // if there's any difference in memory impact. If there is, that could - // indicate a React leak we don't know about. - fiber.return = null; - fiber.dependencies = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.stateNode = null; - // TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead. - fiber.updateQueue = null; - } - } -} - -function emptyPortalContainer(current: Fiber) { - if (!supportsPersistence) { - return; - } - - const portal: { - containerInfo: Container, - pendingChildren: ChildSet, - ... - } = current.stateNode; - const {containerInfo} = portal; - const emptyChildSet = createContainerChildSet(containerInfo); - replaceContainerChildren(containerInfo, emptyChildSet); -} - -function getHostParentFiber(fiber: Fiber): Fiber { - let parent = fiber.return; - while (parent !== null) { - if (isHostParent(parent)) { - return parent; - } - parent = parent.return; - } - - throw new Error( - 'Expected to find a host parent. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); -} - -function isHostParent(fiber: Fiber): boolean { - return ( - fiber.tag === HostComponent || - fiber.tag === HostRoot || - (enableFloat && supportsResources ? fiber.tag === HostResource : false) || - (enableHostSingletons && supportsSingletons - ? fiber.tag === HostSingleton - : false) || - fiber.tag === HostPortal - ); -} - -function getHostSibling(fiber: Fiber): ?Instance { - // We're going to search forward into the tree until we find a sibling host - // node. Unfortunately, if multiple insertions are done in a row we have to - // search past them. This leads to exponential search for the next sibling. - // TODO: Find a more efficient way to do this. - let node: Fiber = fiber; - siblings: while (true) { - // If we didn't find anything, let's try the next sibling. - while (node.sibling === null) { - if (node.return === null || isHostParent(node.return)) { - // If we pop out of the root or hit the parent the fiber we are the - // last sibling. - return null; - } - // $FlowFixMe[incompatible-type] found when upgrading Flow - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - while ( - node.tag !== HostComponent && - node.tag !== HostText && - (!(enableHostSingletons && supportsSingletons) - ? true - : node.tag !== HostSingleton) && - node.tag !== DehydratedFragment - ) { - // If it is not host node and, we might have a host node inside it. - // Try to search down until we find one. - if (node.flags & Placement) { - // If we don't have a child, try the siblings instead. - continue siblings; - } - // If we don't have a child, try the siblings instead. - // We also skip portals because they are not part of this host tree. - if (node.child === null || node.tag === HostPortal) { - continue siblings; - } else { - node.child.return = node; - node = node.child; - } - } - // Check if this host node is stable or about to be placed. - if (!(node.flags & Placement)) { - // Found it! - return node.stateNode; - } - } -} - -function commitPlacement(finishedWork: Fiber): void { - if (!supportsMutation) { - return; - } - - if (enableHostSingletons && supportsSingletons) { - if (finishedWork.tag === HostSingleton) { - // Singletons are already in the Host and don't need to be placed - // Since they operate somewhat like Portals though their children will - // have Placement and will get placed inside them - return; - } - } - // Recursively insert all host nodes into the parent. - const parentFiber = getHostParentFiber(finishedWork); - - switch (parentFiber.tag) { - case HostSingleton: { - if (enableHostSingletons && supportsSingletons) { - const parent: Instance = parentFiber.stateNode; - const before = getHostSibling(finishedWork); - // We only have the top Fiber that was inserted but we need to recurse down its - // children to find all the terminal nodes. - insertOrAppendPlacementNode(finishedWork, before, parent); - break; - } - } - // eslint-disable-next-line no-fallthrough - case HostComponent: { - const parent: Instance = parentFiber.stateNode; - if (parentFiber.flags & ContentReset) { - // Reset the text content of the parent before doing any insertions - resetTextContent(parent); - // Clear ContentReset from the effect tag - parentFiber.flags &= ~ContentReset; - } - - const before = getHostSibling(finishedWork); - // We only have the top Fiber that was inserted but we need to recurse down its - // children to find all the terminal nodes. - insertOrAppendPlacementNode(finishedWork, before, parent); - break; - } - case HostRoot: - case HostPortal: { - const parent: Container = parentFiber.stateNode.containerInfo; - const before = getHostSibling(finishedWork); - insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); - break; - } - // eslint-disable-next-line-no-fallthrough - default: - throw new Error( - 'Invalid host parent fiber. This error is likely caused by a bug ' + - 'in React. Please file an issue.', - ); - } -} - -function insertOrAppendPlacementNodeIntoContainer( - node: Fiber, - before: ?Instance, - parent: Container, -): void { - const {tag} = node; - const isHost = tag === HostComponent || tag === HostText; - if (isHost) { - const stateNode = node.stateNode; - if (before) { - insertInContainerBefore(parent, stateNode, before); - } else { - appendChildToContainer(parent, stateNode); - } - } else if ( - tag === HostPortal || - (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false) - ) { - // If the insertion itself is a portal, then we don't want to traverse - // down its children. Instead, we'll get insertions from each child in - // the portal directly. - // If the insertion is a HostSingleton then it will be placed independently - } else { - const child = node.child; - if (child !== null) { - insertOrAppendPlacementNodeIntoContainer(child, before, parent); - let sibling = child.sibling; - while (sibling !== null) { - insertOrAppendPlacementNodeIntoContainer(sibling, before, parent); - sibling = sibling.sibling; - } - } - } -} - -function insertOrAppendPlacementNode( - node: Fiber, - before: ?Instance, - parent: Instance, -): void { - const {tag} = node; - const isHost = tag === HostComponent || tag === HostText; - if (isHost) { - const stateNode = node.stateNode; - if (before) { - insertBefore(parent, stateNode, before); - } else { - appendChild(parent, stateNode); - } - } else if ( - tag === HostPortal || - (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false) - ) { - // If the insertion itself is a portal, then we don't want to traverse - // down its children. Instead, we'll get insertions from each child in - // the portal directly. - // If the insertion is a HostSingleton then it will be placed independently - } else { - const child = node.child; - if (child !== null) { - insertOrAppendPlacementNode(child, before, parent); - let sibling = child.sibling; - while (sibling !== null) { - insertOrAppendPlacementNode(sibling, before, parent); - sibling = sibling.sibling; - } - } - } -} - -// 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; - -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. - - // 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: null | Fiber = returnFiber; - findParent: while (parent !== null) { - switch (parent.tag) { - case HostSingleton: - 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); - - // 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 HostResource: { - if (enableFloat && supportsResources) { - if (!offscreenSubtreeWasHidden) { - safelyDetachRef(deletedFiber, nearestMountedAncestor); - } - recursivelyTraverseDeletionEffects( - finishedRoot, - nearestMountedAncestor, - deletedFiber, - ); - if (deletedFiber.memoizedState) { - releaseResource(deletedFiber.memoizedState); - } - return; - } - } - // eslint-disable-next-line no-fallthrough - case HostSingleton: { - if (enableHostSingletons && supportsSingletons) { - if (!offscreenSubtreeWasHidden) { - safelyDetachRef(deletedFiber, nearestMountedAncestor); - } - - const prevHostParent = hostParent; - const prevHostParentIsContainer = hostParentIsContainer; - hostParent = deletedFiber.stateNode; - recursivelyTraverseDeletionEffects( - finishedRoot, - nearestMountedAncestor, - deletedFiber, - ); - - // Normally this is called in passive unmount effect phase however with - // HostSingleton we warn if you acquire one that is already associated to - // a different fiber. To increase our chances of avoiding this, specifically - // if you keyed a HostSingleton so there will be a delete followed by a Placement - // we treat detach eagerly here - releaseSingletonInstance(deletedFiber.stateNode); - - hostParent = prevHostParent; - hostParentIsContainer = prevHostParentIsContainer; - - return; - } - } - // eslint-disable-next-line no-fallthrough - 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 { - recursivelyTraverseDeletionEffects( - finishedRoot, - nearestMountedAncestor, - deletedFiber, - ); - } - return; - } - case DehydratedFragment: { - if (enableSuspenseCallback) { - const hydrationCallbacks = finishedRoot.hydrationCallbacks; - if (hydrationCallbacks !== null) { - const onDeleted = hydrationCallbacks.onDeleted; - if (onDeleted) { - onDeleted((deletedFiber.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 { - emptyPortalContainer(deletedFiber); - - recursivelyTraverseDeletionEffects( - finishedRoot, - nearestMountedAncestor, - deletedFiber, - ); - } - 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 (shouldProfile(deletedFiber)) { - startLayoutEffectTimer(); - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - recordLayoutEffectDuration(deletedFiber); - } else { - safelyCallDestroy( - deletedFiber, - nearestMountedAncestor, - destroy, - ); - } - - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStopped(); - } - } - } - effect = effect.next; - } while (effect !== firstEffect); - } - } - } - - 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; - } - case ScopeComponent: { - if (enableScopeAPI) { - safelyDetachRef(deletedFiber, nearestMountedAncestor); - } - recursivelyTraverseDeletionEffects( - finishedRoot, - nearestMountedAncestor, - deletedFiber, - ); - return; - } - case OffscreenComponent: { - safelyDetachRef(deletedFiber, nearestMountedAncestor); - if (deletedFiber.mode & ConcurrentMode) { - // 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; - } else { - recursivelyTraverseDeletionEffects( - finishedRoot, - nearestMountedAncestor, - deletedFiber, - ); - } - 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)); - } - } else if (__DEV__) { - if (suspenseCallback !== undefined) { - console.error('Unexpected type for suspenseCallback.'); - } - } - } -} - -function commitSuspenseHydrationCallbacks( - finishedRoot: FiberRoot, - finishedWork: Fiber, -) { - if (!supportsHydration) { - return; - } - const newState: SuspenseState | null = finishedWork.memoizedState; - if (newState === null) { - const current = finishedWork.alternate; - if (current !== null) { - const prevState: SuspenseState | null = current.memoizedState; - if (prevState !== null) { - const suspenseInstance = prevState.dehydrated; - if (suspenseInstance !== null) { - try { - commitHydratedSuspenseInstance(suspenseInstance); - if (enableSuspenseCallback) { - const hydrationCallbacks = finishedRoot.hydrationCallbacks; - if (hydrationCallbacks !== null) { - const onHydrated = hydrationCallbacks.onHydrated; - if (onHydrated) { - onHydrated(suspenseInstance); - } - } - } - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } - } - } - } -} - -function getRetryCache(finishedWork) { - // TODO: Unify the interface for the retry cache so we don't have to switch - // on the tag like this. - switch (finishedWork.tag) { - case SuspenseComponent: - case SuspenseListComponent: { - let retryCache = finishedWork.stateNode; - if (retryCache === null) { - retryCache = finishedWork.stateNode = new PossiblyWeakSet(); - } - return retryCache; - } - case OffscreenComponent: { - const instance: OffscreenInstance = finishedWork.stateNode; - // $FlowFixMe[incompatible-type-arg] found when upgrading Flow - let retryCache: null | Set | WeakSet = - // $FlowFixMe[incompatible-type] found when upgrading Flow - instance._retryCache; - if (retryCache === null) { - // $FlowFixMe[incompatible-type] - retryCache = instance._retryCache = new PossiblyWeakSet(); - } - return retryCache; - } - default: { - throw new Error( - `Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` + - 'bug in React.', - ); - } - } -} - -export function detachOffscreenInstance(instance: OffscreenInstance): void { - const fiber = instance._current; - if (fiber === null) { - throw new Error( - 'Calling Offscreen.detach before instance handle has been set.', - ); - } - - if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) { - // The instance is already detached, this is a noop. - return; - } - - const root = enqueueConcurrentRenderForLane(fiber, SyncLane); - if (root !== null) { - instance._pendingVisibility |= OffscreenDetached; - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); - } -} - -export function attachOffscreenInstance(instance: OffscreenInstance): void { - const fiber = instance._current; - if (fiber === null) { - throw new Error( - 'Calling Offscreen.detach before instance handle has been set.', - ); - } - - if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) { - // The instance is already attached, this is a noop. - return; - } - - const root = enqueueConcurrentRenderForLane(fiber, SyncLane); - if (root !== null) { - instance._pendingVisibility &= ~OffscreenDetached; - scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); - } -} - -function attachSuspenseRetryListeners( - finishedWork: Fiber, - wakeables: Set, -) { - // If this boundary just timed out, then it will have a set of wakeables. - // For each wakeable, attach a listener so that when it resolves, React - // attempts to re-render the boundary in the primary (pre-timeout) state. - const retryCache = getRetryCache(finishedWork); - wakeables.forEach(wakeable => { - // Memoize using the boundary fiber to prevent redundant listeners. - const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable); - if (!retryCache.has(wakeable)) { - retryCache.add(wakeable); - - if (enableUpdaterTracking) { - if (isDevToolsPresent) { - if (inProgressLanes !== null && inProgressRoot !== null) { - // If we have pending work still, associate the original updaters with it. - restorePendingUpdaters(inProgressRoot, inProgressLanes); - } else { - throw Error( - 'Expected finished root and lanes to be set. This is a bug in React.', - ); - } - } - } - - wakeable.then(retry, retry); - } - }); -} - -// This function detects when a Suspense boundary goes from visible to hidden. -// It returns false if the boundary is already hidden. -// TODO: Use an effect tag. -export function isSuspenseBoundaryBeingHidden( - current: Fiber | null, - finishedWork: Fiber, -): boolean { - if (current !== null) { - const oldState: SuspenseState | null = current.memoizedState; - if (oldState === null || oldState.dehydrated !== null) { - const newState: SuspenseState | null = finishedWork.memoizedState; - return newState !== null && newState.dehydrated === null; - } - } - return false; -} - -export function commitMutationEffects( - root: FiberRoot, - finishedWork: Fiber, - committedLanes: Lanes, -) { - inProgressLanes = committedLanes; - inProgressRoot = root; - - setCurrentDebugFiberInDEV(finishedWork); - commitMutationEffectsOnFiber(finishedWork, root, committedLanes); - setCurrentDebugFiberInDEV(finishedWork); - - inProgressLanes = null; - inProgressRoot = null; -} - -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 prevDebugFiber = getCurrentDebugFiberInDEV(); - if (parentFiber.subtreeFlags & MutationMask) { - let child = parentFiber.child; - while (child !== null) { - setCurrentDebugFiberInDEV(child); - commitMutationEffectsOnFiber(child, root, lanes); - child = child.sibling; - } - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -function commitMutationEffectsOnFiber( - finishedWork: Fiber, - root: FiberRoot, - lanes: Lanes, -) { - const current = finishedWork.alternate; - const flags = finishedWork.flags; - - // 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 reconciliation, 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 & 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 (shouldProfile(finishedWork)) { - 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; - } - case ClassComponent: { - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - commitReconciliationEffects(finishedWork); - - if (flags & Ref) { - if (current !== null) { - safelyDetachRef(current, current.return); - } - } - - if (flags & Callback && offscreenSubtreeIsHidden) { - const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - deferHiddenCallbacks(updateQueue); - } - } - return; - } - case HostResource: { - if (enableFloat && supportsResources) { - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - commitReconciliationEffects(finishedWork); - - if (flags & Ref) { - if (current !== null) { - safelyDetachRef(current, current.return); - } - } - - if (flags & Update) { - const newResource = finishedWork.memoizedState; - if (current !== null) { - const currentResource = current.memoizedState; - if (currentResource !== newResource) { - releaseResource(currentResource); - } - } - finishedWork.stateNode = newResource - ? acquireResource(newResource) - : null; - } - return; - } - } - // eslint-disable-next-line-no-fallthrough - case HostSingleton: { - if (enableHostSingletons && supportsSingletons) { - if (flags & Update) { - const previousWork = finishedWork.alternate; - if (previousWork === null) { - const singleton = finishedWork.stateNode; - const props = finishedWork.memoizedProps; - // This was a new mount, we need to clear and set initial properties - clearSingleton(singleton); - acquireSingletonInstance( - finishedWork.type, - props, - singleton, - finishedWork, - ); - } - } - } - } - // eslint-disable-next-line-no-fallthrough - 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 & 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); - - const offscreenFiber: Fiber = (finishedWork.child: any); - - if (offscreenFiber.flags & Visibility) { - const newState: OffscreenState | null = offscreenFiber.memoizedState; - const isHidden = newState !== null; - if (isHidden) { - const wasHidden = - offscreenFiber.alternate !== null && - offscreenFiber.alternate.memoizedState !== null; - if (!wasHidden) { - // TODO: Move to passive phase - markCommitTimeOfFallback(); - } - } - } - - if (flags & Update) { - try { - commitSuspenseCallback(finishedWork); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - const wakeables: Set | null = (finishedWork.updateQueue: any); - if (wakeables !== null) { - finishedWork.updateQueue = null; - attachSuspenseRetryListeners(finishedWork, wakeables); - } - } - return; - } - case OffscreenComponent: { - if (flags & Ref) { - if (current !== null) { - safelyDetachRef(current, current.return); - } - } - - const newState: OffscreenState | null = finishedWork.memoizedState; - const isHidden = newState !== null; - const wasHidden = current !== null && current.memoizedState !== null; - - if (finishedWork.mode & ConcurrentMode) { - // 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 prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden; - const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; - offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden || isHidden; - offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || wasHidden; - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden; - offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden; - } else { - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - } - - commitReconciliationEffects(finishedWork); - // TODO: Add explicit effect flag to set _current. - finishedWork.stateNode._current = finishedWork; - - if (flags & Visibility) { - const offscreenInstance: OffscreenInstance = finishedWork.stateNode; - - // Track the current state on the Offscreen instance so we can - // read it during an event - if (isHidden) { - offscreenInstance._visibility &= ~OffscreenVisible; - } else { - offscreenInstance._visibility |= OffscreenVisible; - } - - if (isHidden) { - const isUpdate = current !== null; - const wasHiddenByAncestorOffscreen = - offscreenSubtreeIsHidden || offscreenSubtreeWasHidden; - // Only trigger disapper layout effects if: - // - This is an update, not first mount. - // - This Offscreen was not hidden before. - // - Ancestor Offscreen was not hidden in previous commit. - if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) { - if ((finishedWork.mode & ConcurrentMode) !== NoMode) { - // Disappear the layout effects of all the children - recursivelyTraverseDisappearLayoutEffects(finishedWork); - } - } - } else { - if (wasHidden) { - // TODO: Move re-appear call here for symmetry? - } - } - - // Offscreen with manual mode manages visibility manually. - if (supportsMutation && !isOffscreenManual(finishedWork)) { - // TODO: This needs to run whenever there's an insertion or update - // inside a hidden Offscreen tree. - hideOrUnhideAllChildren(finishedWork, isHidden); - } - } - - // TODO: Move to passive phase - if (flags & Update) { - const offscreenQueue: OffscreenQueue | null = (finishedWork.updateQueue: any); - if (offscreenQueue !== null) { - const wakeables = offscreenQueue.wakeables; - if (wakeables !== null) { - offscreenQueue.wakeables = null; - attachSuspenseRetryListeners(finishedWork, wakeables); - } - } - } - return; - } - case SuspenseListComponent: { - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - commitReconciliationEffects(finishedWork); - - if (flags & Update) { - const wakeables: Set | null = (finishedWork.updateQueue: any); - if (wakeables !== null) { - finishedWork.updateQueue = null; - attachSuspenseRetryListeners(finishedWork, wakeables); - } - } - return; - } - case ScopeComponent: { - if (enableScopeAPI) { - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - commitReconciliationEffects(finishedWork); - - // 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; - } - default: { - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - commitReconciliationEffects(finishedWork); - - return; - } - } -} -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; - } -} - -export function commitLayoutEffects( - finishedWork: Fiber, - root: FiberRoot, - committedLanes: Lanes, -): void { - inProgressLanes = committedLanes; - inProgressRoot = root; - - const current = finishedWork.alternate; - commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes); - - inProgressLanes = null; - inProgressRoot = null; -} - -function recursivelyTraverseLayoutEffects( - root: FiberRoot, - parentFiber: Fiber, - lanes: Lanes, -) { - const prevDebugFiber = getCurrentDebugFiberInDEV(); - if (parentFiber.subtreeFlags & LayoutMask) { - let child = parentFiber.child; - while (child !== null) { - setCurrentDebugFiberInDEV(child); - const current = child.alternate; - commitLayoutEffectOnFiber(root, current, child, lanes); - child = child.sibling; - } - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -export function disappearLayoutEffects(finishedWork: Fiber) { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case MemoComponent: - case SimpleMemoComponent: { - // TODO (Offscreen) Check: flags & LayoutStatic - if (shouldProfile(finishedWork)) { - try { - startLayoutEffectTimer(); - commitHookEffectListUnmount( - HookLayout, - finishedWork, - finishedWork.return, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListUnmount( - HookLayout, - finishedWork, - finishedWork.return, - ); - } - - recursivelyTraverseDisappearLayoutEffects(finishedWork); - break; - } - case ClassComponent: { - // TODO (Offscreen) Check: flags & RefStatic - safelyDetachRef(finishedWork, finishedWork.return); - - const instance = finishedWork.stateNode; - if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount( - finishedWork, - finishedWork.return, - instance, - ); - } - - recursivelyTraverseDisappearLayoutEffects(finishedWork); - break; - } - case HostResource: - case HostSingleton: - case HostComponent: { - // TODO (Offscreen) Check: flags & RefStatic - safelyDetachRef(finishedWork, finishedWork.return); - - recursivelyTraverseDisappearLayoutEffects(finishedWork); - break; - } - case OffscreenComponent: { - // TODO (Offscreen) Check: flags & RefStatic - safelyDetachRef(finishedWork, finishedWork.return); - - const isHidden = finishedWork.memoizedState !== null; - if (isHidden) { - // Nested Offscreen tree is already hidden. Don't disappear - // its effects. - } else { - recursivelyTraverseDisappearLayoutEffects(finishedWork); - } - break; - } - default: { - recursivelyTraverseDisappearLayoutEffects(finishedWork); - break; - } - } -} - -function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) { - // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic) - let child = parentFiber.child; - while (child !== null) { - disappearLayoutEffects(child); - child = child.sibling; - } -} - -export function reappearLayoutEffects( - finishedRoot: FiberRoot, - current: Fiber | null, - finishedWork: Fiber, - // This function visits both newly finished work and nodes that were re-used - // from a previously committed tree. We cannot check non-static flags if the - // node was reused. - includeWorkInProgressEffects: boolean, -) { - // Turn on layout effects in a tree that previously disappeared. - const flags = finishedWork.flags; - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - // TODO: Check flags & LayoutStatic - commitHookLayoutEffects(finishedWork, HookLayout); - break; - } - case ClassComponent: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - - // TODO: Check for LayoutStatic flag - const instance = finishedWork.stateNode; - if (typeof instance.componentDidMount === 'function') { - try { - instance.componentDidMount(); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } - - // Commit any callbacks that would have fired while the component - // was hidden. - const updateQueue: UpdateQueue | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - commitHiddenCallbacks(updateQueue, instance); - } - - // If this is newly finished work, check for setState callbacks - if (includeWorkInProgressEffects && flags & Callback) { - commitClassCallbacks(finishedWork); - } - - // TODO: Check flags & RefStatic - safelyAttachRef(finishedWork, finishedWork.return); - break; - } - // Unlike commitLayoutEffectsOnFiber, we don't need to handle HostRoot - // because this function only visits nodes that are inside an - // Offscreen fiber. - // case HostRoot: { - // ... - // } - case HostResource: - case HostSingleton: - case HostComponent: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - - // Renderers may schedule work to be done after host components are mounted - // (eg DOM renderer may schedule auto-focus for inputs and form controls). - // These effects should only be committed when components are first mounted, - // aka when there is no current/alternate. - if (includeWorkInProgressEffects && current === null && flags & Update) { - commitHostComponentMount(finishedWork); - } - - // TODO: Check flags & Ref - safelyAttachRef(finishedWork, finishedWork.return); - break; - } - case Profiler: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - // TODO: Figure out how Profiler updates should work with Offscreen - if (includeWorkInProgressEffects && flags & Update) { - commitProfilerUpdate(finishedWork, current); - } - break; - } - case SuspenseComponent: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - - // TODO: Figure out how Suspense hydration callbacks should work - // with Offscreen. - if (includeWorkInProgressEffects && flags & Update) { - commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); - } - break; - } - case OffscreenComponent: { - const offscreenState: OffscreenState = finishedWork.memoizedState; - const isHidden = offscreenState !== null; - if (isHidden) { - // Nested Offscreen tree is still hidden. Don't re-appear its effects. - } else { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - } - // TODO: Check flags & Ref - safelyAttachRef(finishedWork, finishedWork.return); - break; - } - default: { - recursivelyTraverseReappearLayoutEffects( - finishedRoot, - finishedWork, - includeWorkInProgressEffects, - ); - break; - } - } -} - -function recursivelyTraverseReappearLayoutEffects( - finishedRoot: FiberRoot, - parentFiber: Fiber, - includeWorkInProgressEffects: boolean, -) { - // This function visits both newly finished work and nodes that were re-used - // from a previously committed tree. We cannot check non-static flags if the - // node was reused. - const childShouldIncludeWorkInProgressEffects = - includeWorkInProgressEffects && - (parentFiber.subtreeFlags & LayoutMask) !== NoFlags; - - // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic) - const prevDebugFiber = getCurrentDebugFiberInDEV(); - let child = parentFiber.child; - while (child !== null) { - const current = child.alternate; - reappearLayoutEffects( - finishedRoot, - current, - child, - childShouldIncludeWorkInProgressEffects, - ); - child = child.sibling; - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -function commitHookPassiveMountEffects( - finishedWork: Fiber, - hookFlags: HookFlags, -) { - if (shouldProfile(finishedWork)) { - startPassiveEffectTimer(); - try { - commitHookEffectListMount(hookFlags, finishedWork); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - recordPassiveEffectDuration(finishedWork); - } else { - try { - commitHookEffectListMount(hookFlags, finishedWork); - } catch (error) { - captureCommitPhaseError(finishedWork, finishedWork.return, error); - } - } -} - -function commitOffscreenPassiveMountEffects( - current: Fiber | null, - finishedWork: Fiber, - instance: OffscreenInstance, -) { - if (enableCache) { - let previousCache: Cache | null = null; - if ( - current !== null && - current.memoizedState !== null && - current.memoizedState.cachePool !== null - ) { - previousCache = current.memoizedState.cachePool.pool; - } - let nextCache: Cache | null = null; - if ( - finishedWork.memoizedState !== null && - finishedWork.memoizedState.cachePool !== null - ) { - nextCache = finishedWork.memoizedState.cachePool.pool; - } - // Retain/release the cache used for pending (suspended) nodes. - // Note that this is only reached in the non-suspended/visible case: - // when the content is suspended/hidden, the retain/release occurs - // via the parent Suspense component (see case above). - if (nextCache !== previousCache) { - if (nextCache != null) { - retainCache(nextCache); - } - if (previousCache != null) { - releaseCache(previousCache); - } - } - } - - if (enableTransitionTracing) { - // TODO: Pre-rendering should not be counted as part of a transition. We - // may add separate logs for pre-rendering, but it's not part of the - // primary metrics. - const offscreenState: OffscreenState = finishedWork.memoizedState; - const queue: OffscreenQueue | null = (finishedWork.updateQueue: any); - - const isHidden = offscreenState !== null; - if (queue !== null) { - if (isHidden) { - const transitions = queue.transitions; - if (transitions !== null) { - transitions.forEach(transition => { - // Add all the transitions saved in the update queue during - // the render phase (ie the transitions associated with this boundary) - // into the transitions set. - if (instance._transitions === null) { - instance._transitions = new Set(); - } - instance._transitions.add(transition); - }); - } - - const markerInstances = queue.markerInstances; - if (markerInstances !== null) { - markerInstances.forEach(markerInstance => { - const markerTransitions = markerInstance.transitions; - // There should only be a few tracing marker transitions because - // they should be only associated with the transition that - // caused them - if (markerTransitions !== null) { - markerTransitions.forEach(transition => { - if (instance._transitions === null) { - instance._transitions = new Set(); - } else if (instance._transitions.has(transition)) { - if (markerInstance.pendingBoundaries === null) { - markerInstance.pendingBoundaries = new Map(); - } - if (instance._pendingMarkers === null) { - instance._pendingMarkers = new Set(); - } - - instance._pendingMarkers.add(markerInstance); - } - }); - } - }); - } - } - - finishedWork.updateQueue = null; - } - - commitTransitionProgress(finishedWork); - - // TODO: Refactor this into an if/else branch - if (!isHidden) { - instance._transitions = null; - instance._pendingMarkers = null; - } - } -} - -function commitCachePassiveMountEffect( - current: Fiber | null, - finishedWork: Fiber, -) { - if (enableCache) { - let previousCache: Cache | null = null; - if (finishedWork.alternate !== null) { - previousCache = finishedWork.alternate.memoizedState.cache; - } - const nextCache = finishedWork.memoizedState.cache; - // Retain/release the cache. In theory the cache component - // could be "borrowing" a cache instance owned by some parent, - // in which case we could avoid retaining/releasing. But it - // is non-trivial to determine when that is the case, so we - // always retain/release. - if (nextCache !== previousCache) { - retainCache(nextCache); - if (previousCache != null) { - releaseCache(previousCache); - } - } - } -} - -function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) { - // Get the transitions that were initiatized during the render - // and add a start transition callback for each of them - // We will only call this on initial mount of the tracing marker - // only if there are no suspense children - const instance = finishedWork.stateNode; - if (instance.transitions !== null && instance.pendingBoundaries === null) { - addMarkerCompleteCallbackToPendingTransition( - finishedWork.memoizedProps.name, - instance.transitions, - ); - instance.transitions = null; - instance.pendingBoundaries = null; - instance.aborts = null; - instance.name = null; - } -} - -export function commitPassiveMountEffects( - root: FiberRoot, - finishedWork: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, -): void { - setCurrentDebugFiberInDEV(finishedWork); - commitPassiveMountOnFiber( - root, - finishedWork, - committedLanes, - committedTransitions, - ); - resetCurrentDebugFiberInDEV(); -} - -function recursivelyTraversePassiveMountEffects( - root: FiberRoot, - parentFiber: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, -) { - const prevDebugFiber = getCurrentDebugFiberInDEV(); - if (parentFiber.subtreeFlags & PassiveMask) { - let child = parentFiber.child; - while (child !== null) { - setCurrentDebugFiberInDEV(child); - commitPassiveMountOnFiber( - root, - child, - committedLanes, - committedTransitions, - ); - child = child.sibling; - } - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -function commitPassiveMountOnFiber( - finishedRoot: FiberRoot, - finishedWork: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, -): void { - // When updating this function, also update reconnectPassiveEffects, which does - // most of the same things when an offscreen tree goes from hidden -> visible, - // or when toggling effects inside a hidden tree. - const flags = finishedWork.flags; - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - commitHookPassiveMountEffects( - finishedWork, - HookPassive | HookHasEffect, - ); - } - break; - } - case HostRoot: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - if (enableCache) { - let previousCache: Cache | null = null; - if (finishedWork.alternate !== null) { - previousCache = finishedWork.alternate.memoizedState.cache; - } - const nextCache = finishedWork.memoizedState.cache; - // Retain/release the root cache. - // Note that on initial mount, previousCache and nextCache will be the same - // and this retain won't occur. To counter this, we instead retain the HostRoot's - // initial cache when creating the root itself (see createFiberRoot() in - // ReactFiberRoot.js). Subsequent updates that change the cache are reflected - // here, such that previous/next caches are retained correctly. - if (nextCache !== previousCache) { - retainCache(nextCache); - if (previousCache != null) { - releaseCache(previousCache); - } - } - } - - if (enableTransitionTracing) { - // Get the transitions that were initiatized during the render - // and add a start transition callback for each of them - const root: FiberRoot = finishedWork.stateNode; - const incompleteTransitions = root.incompleteTransitions; - // Initial render - if (committedTransitions !== null) { - committedTransitions.forEach(transition => { - addTransitionStartCallbackToPendingTransition(transition); - }); - - clearTransitionsForLanes(finishedRoot, committedLanes); - } - - incompleteTransitions.forEach((markerInstance, transition) => { - const pendingBoundaries = markerInstance.pendingBoundaries; - if (pendingBoundaries === null || pendingBoundaries.size === 0) { - if (markerInstance.aborts === null) { - addTransitionCompleteCallbackToPendingTransition(transition); - } - incompleteTransitions.delete(transition); - } - }); - - clearTransitionsForLanes(finishedRoot, committedLanes); - } - } - break; - } - case LegacyHiddenComponent: { - if (enableLegacyHidden) { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - - if (flags & Passive) { - const current = finishedWork.alternate; - const instance: OffscreenInstance = finishedWork.stateNode; - commitOffscreenPassiveMountEffects(current, finishedWork, instance); - } - } - break; - } - case OffscreenComponent: { - // TODO: Pass `current` as argument to this function - const instance: OffscreenInstance = finishedWork.stateNode; - const nextState: OffscreenState | null = finishedWork.memoizedState; - - const isHidden = nextState !== null; - - if (isHidden) { - if (instance._visibility & OffscreenPassiveEffectsConnected) { - // The effects are currently connected. Update them. - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - } else { - if (finishedWork.mode & ConcurrentMode) { - // The effects are currently disconnected. Since the tree is hidden, - // don't connect them. This also applies to the initial render. - if (enableCache || enableTransitionTracing) { - // "Atomic" effects are ones that need to fire on every commit, - // even during pre-rendering. An example is updating the reference - // count on cache instances. - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - } - } else { - // Legacy Mode: Fire the effects even if the tree is hidden. - instance._visibility |= OffscreenPassiveEffectsConnected; - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - } - } - } else { - // Tree is visible - if (instance._visibility & OffscreenPassiveEffectsConnected) { - // The effects are currently connected. Update them. - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - } else { - // The effects are currently disconnected. Reconnect them, while also - // firing effects inside newly mounted trees. This also applies to - // the initial render. - instance._visibility |= OffscreenPassiveEffectsConnected; - - const includeWorkInProgressEffects = - (finishedWork.subtreeFlags & PassiveMask) !== NoFlags; - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - } - } - - if (flags & Passive) { - const current = finishedWork.alternate; - commitOffscreenPassiveMountEffects(current, finishedWork, instance); - } - break; - } - case CacheComponent: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - // TODO: Pass `current` as argument to this function - const current = finishedWork.alternate; - commitCachePassiveMountEffect(current, finishedWork); - } - break; - } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - commitTracingMarkerPassiveMountEffect(finishedWork); - } - break; - } - // Intentional fallthrough to next branch - } - // eslint-disable-next-line-no-fallthrough - default: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - break; - } - } -} - -function recursivelyTraverseReconnectPassiveEffects( - finishedRoot: FiberRoot, - parentFiber: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, - includeWorkInProgressEffects: boolean, -) { - // This function visits both newly finished work and nodes that were re-used - // from a previously committed tree. We cannot check non-static flags if the - // node was reused. - const childShouldIncludeWorkInProgressEffects = - includeWorkInProgressEffects && - (parentFiber.subtreeFlags & PassiveMask) !== NoFlags; - - // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic) - const prevDebugFiber = getCurrentDebugFiberInDEV(); - let child = parentFiber.child; - while (child !== null) { - reconnectPassiveEffects( - finishedRoot, - child, - committedLanes, - committedTransitions, - childShouldIncludeWorkInProgressEffects, - ); - child = child.sibling; - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -export function reconnectPassiveEffects( - finishedRoot: FiberRoot, - finishedWork: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, - // This function visits both newly finished work and nodes that were re-used - // from a previously committed tree. We cannot check non-static flags if the - // node was reused. - includeWorkInProgressEffects: boolean, -) { - const flags = finishedWork.flags; - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - // TODO: Check for PassiveStatic flag - commitHookPassiveMountEffects(finishedWork, HookPassive); - break; - } - // Unlike commitPassiveMountOnFiber, we don't need to handle HostRoot - // because this function only visits nodes that are inside an - // Offscreen fiber. - // case HostRoot: { - // ... - // } - case LegacyHiddenComponent: { - if (enableLegacyHidden) { - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - - if (includeWorkInProgressEffects && flags & Passive) { - // TODO: Pass `current` as argument to this function - const current: Fiber | null = finishedWork.alternate; - const instance: OffscreenInstance = finishedWork.stateNode; - commitOffscreenPassiveMountEffects(current, finishedWork, instance); - } - } - break; - } - case OffscreenComponent: { - const instance: OffscreenInstance = finishedWork.stateNode; - const nextState: OffscreenState | null = finishedWork.memoizedState; - - const isHidden = nextState !== null; - - if (isHidden) { - if (instance._visibility & OffscreenPassiveEffectsConnected) { - // The effects are currently connected. Update them. - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - } else { - if (finishedWork.mode & ConcurrentMode) { - // The effects are currently disconnected. Since the tree is hidden, - // don't connect them. This also applies to the initial render. - if (enableCache || enableTransitionTracing) { - // "Atomic" effects are ones that need to fire on every commit, - // even during pre-rendering. An example is updating the reference - // count on cache instances. - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - } - } else { - // Legacy Mode: Fire the effects even if the tree is hidden. - instance._visibility |= OffscreenPassiveEffectsConnected; - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - } - } - } else { - // Tree is visible - - // Since we're already inside a reconnecting tree, it doesn't matter - // whether the effects are currently connected. In either case, we'll - // continue traversing the tree and firing all the effects. - // - // We do need to set the "connected" flag on the instance, though. - instance._visibility |= OffscreenPassiveEffectsConnected; - - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - } - - if (includeWorkInProgressEffects && flags & Passive) { - // TODO: Pass `current` as argument to this function - const current: Fiber | null = finishedWork.alternate; - commitOffscreenPassiveMountEffects(current, finishedWork, instance); - } - break; - } - case CacheComponent: { - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - if (includeWorkInProgressEffects && flags & Passive) { - // TODO: Pass `current` as argument to this function - const current = finishedWork.alternate; - commitCachePassiveMountEffect(current, finishedWork); - } - break; - } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - if (includeWorkInProgressEffects && flags & Passive) { - commitTracingMarkerPassiveMountEffect(finishedWork); - } - break; - } - // Intentional fallthrough to next branch - } - // eslint-disable-next-line-no-fallthrough - default: { - recursivelyTraverseReconnectPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - includeWorkInProgressEffects, - ); - break; - } - } -} - -function recursivelyTraverseAtomicPassiveEffects( - finishedRoot: FiberRoot, - parentFiber: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, -) { - // "Atomic" effects are ones that need to fire on every commit, even during - // pre-rendering. We call this function when traversing a hidden tree whose - // regular effects are currently disconnected. - const prevDebugFiber = getCurrentDebugFiberInDEV(); - // TODO: Add special flag for atomic effects - if (parentFiber.subtreeFlags & PassiveMask) { - let child = parentFiber.child; - while (child !== null) { - setCurrentDebugFiberInDEV(child); - commitAtomicPassiveEffects( - finishedRoot, - child, - committedLanes, - committedTransitions, - ); - child = child.sibling; - } - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -function commitAtomicPassiveEffects( - finishedRoot: FiberRoot, - finishedWork: Fiber, - committedLanes: Lanes, - committedTransitions: Array | null, -) { - // "Atomic" effects are ones that need to fire on every commit, even during - // pre-rendering. We call this function when traversing a hidden tree whose - // regular effects are currently disconnected. - const flags = finishedWork.flags; - switch (finishedWork.tag) { - case OffscreenComponent: { - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - // TODO: Pass `current` as argument to this function - const current = finishedWork.alternate; - const instance: OffscreenInstance = finishedWork.stateNode; - commitOffscreenPassiveMountEffects(current, finishedWork, instance); - } - break; - } - case CacheComponent: { - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - // TODO: Pass `current` as argument to this function - const current = finishedWork.alternate; - commitCachePassiveMountEffect(current, finishedWork); - } - break; - } - // eslint-disable-next-line-no-fallthrough - default: { - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - break; - } - } -} - -export function commitPassiveUnmountEffects(finishedWork: Fiber): void { - setCurrentDebugFiberInDEV(finishedWork); - commitPassiveUnmountOnFiber(finishedWork); - resetCurrentDebugFiberInDEV(); -} - -function detachAlternateSiblings(parentFiber: Fiber) { - if (deletedTreeCleanUpLevel >= 1) { - // A fiber was deleted from this parent fiber, but it's still part of the - // previous (alternate) parent fiber's list of children. Because children - // are a linked list, an earlier sibling that's still alive will be - // connected to the deleted fiber via its `alternate`: - // - // live fiber --alternate--> previous live fiber --sibling--> deleted - // fiber - // - // We can't disconnect `alternate` on nodes that haven't been deleted yet, - // but we can disconnect the `sibling` and `child` pointers. - - const previousFiber = parentFiber.alternate; - if (previousFiber !== null) { - let detachedChild = previousFiber.child; - if (detachedChild !== null) { - previousFiber.child = null; - do { - // $FlowFixMe[incompatible-use] found when upgrading Flow - const detachedSibling = detachedChild.sibling; - // $FlowFixMe[incompatible-use] found when upgrading Flow - detachedChild.sibling = null; - detachedChild = detachedSibling; - } while (detachedChild !== null); - } - } - } -} - -function commitHookPassiveUnmountEffects( - finishedWork: Fiber, - nearestMountedAncestor, - hookFlags: HookFlags, -) { - if (shouldProfile(finishedWork)) { - startPassiveEffectTimer(); - commitHookEffectListUnmount( - hookFlags, - finishedWork, - nearestMountedAncestor, - ); - recordPassiveEffectDuration(finishedWork); - } else { - commitHookEffectListUnmount( - hookFlags, - finishedWork, - nearestMountedAncestor, - ); - } -} - -function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void { - // Deletions effects can be scheduled on any fiber type. They need to happen - // before the children effects have fired. - const deletions = parentFiber.deletions; - - if ((parentFiber.flags & ChildDeletion) !== NoFlags) { - if (deletions !== null) { - for (let i = 0; i < deletions.length; i++) { - const childToDelete = deletions[i]; - // TODO: Convert this to use recursion - nextEffect = childToDelete; - commitPassiveUnmountEffectsInsideOfDeletedTree_begin( - childToDelete, - parentFiber, - ); - } - } - detachAlternateSiblings(parentFiber); - } - - const prevDebugFiber = getCurrentDebugFiberInDEV(); - // TODO: Split PassiveMask into separate masks for mount and unmount? - if (parentFiber.subtreeFlags & PassiveMask) { - let child = parentFiber.child; - while (child !== null) { - setCurrentDebugFiberInDEV(child); - commitPassiveUnmountOnFiber(child); - child = child.sibling; - } - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - recursivelyTraversePassiveUnmountEffects(finishedWork); - if (finishedWork.flags & Passive) { - commitHookPassiveUnmountEffects( - finishedWork, - finishedWork.return, - HookPassive | HookHasEffect, - ); - } - break; - } - case OffscreenComponent: { - const instance: OffscreenInstance = finishedWork.stateNode; - const nextState: OffscreenState | null = finishedWork.memoizedState; - - const isHidden = nextState !== null; - - if ( - isHidden && - instance._visibility & OffscreenPassiveEffectsConnected && - // For backwards compatibility, don't unmount when a tree suspends. In - // the future we may change this to unmount after a delay. - (finishedWork.return === null || - finishedWork.return.tag !== SuspenseComponent) - ) { - // The effects are currently connected. Disconnect them. - // TODO: Add option or heuristic to delay before disconnecting the - // effects. Then if the tree reappears before the delay has elapsed, we - // can skip toggling the effects entirely. - instance._visibility &= ~OffscreenPassiveEffectsConnected; - recursivelyTraverseDisconnectPassiveEffects(finishedWork); - } else { - recursivelyTraversePassiveUnmountEffects(finishedWork); - } - - break; - } - default: { - recursivelyTraversePassiveUnmountEffects(finishedWork); - break; - } - } -} - -function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void { - // Deletions effects can be scheduled on any fiber type. They need to happen - // before the children effects have fired. - const deletions = parentFiber.deletions; - - if ((parentFiber.flags & ChildDeletion) !== NoFlags) { - if (deletions !== null) { - for (let i = 0; i < deletions.length; i++) { - const childToDelete = deletions[i]; - // TODO: Convert this to use recursion - nextEffect = childToDelete; - commitPassiveUnmountEffectsInsideOfDeletedTree_begin( - childToDelete, - parentFiber, - ); - } - } - detachAlternateSiblings(parentFiber); - } - - const prevDebugFiber = getCurrentDebugFiberInDEV(); - // TODO: Check PassiveStatic flag - let child = parentFiber.child; - while (child !== null) { - setCurrentDebugFiberInDEV(child); - disconnectPassiveEffect(child); - child = child.sibling; - } - setCurrentDebugFiberInDEV(prevDebugFiber); -} - -export function disconnectPassiveEffect(finishedWork: Fiber): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - // TODO: Check PassiveStatic flag - commitHookPassiveUnmountEffects( - finishedWork, - finishedWork.return, - HookPassive, - ); - // When disconnecting passive effects, we fire the effects in the same - // order as during a deletiong: parent before child - recursivelyTraverseDisconnectPassiveEffects(finishedWork); - break; - } - case OffscreenComponent: { - const instance: OffscreenInstance = finishedWork.stateNode; - if (instance._visibility & OffscreenPassiveEffectsConnected) { - instance._visibility &= ~OffscreenPassiveEffectsConnected; - recursivelyTraverseDisconnectPassiveEffects(finishedWork); - } else { - // The effects are already disconnected. - } - break; - } - default: { - recursivelyTraverseDisconnectPassiveEffects(finishedWork); - break; - } - } -} - -function commitPassiveUnmountEffectsInsideOfDeletedTree_begin( - deletedSubtreeRoot: Fiber, - nearestMountedAncestor: Fiber | null, -) { - while (nextEffect !== null) { - const fiber = nextEffect; - - // Deletion effects fire in parent -> child order - // TODO: Check if fiber has a PassiveStatic flag - setCurrentDebugFiberInDEV(fiber); - commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor); - resetCurrentDebugFiberInDEV(); - - const child = fiber.child; - // TODO: Only traverse subtree if it has a PassiveStatic flag. (But, if we - // do this, still need to handle `deletedTreeCleanUpLevel` correctly.) - if (child !== null) { - child.return = fiber; - nextEffect = child; - } else { - commitPassiveUnmountEffectsInsideOfDeletedTree_complete( - deletedSubtreeRoot, - ); - } - } -} - -function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( - deletedSubtreeRoot: Fiber, -) { - while (nextEffect !== null) { - const fiber = nextEffect; - const sibling = fiber.sibling; - const returnFiber = fiber.return; - - if (deletedTreeCleanUpLevel >= 2) { - // Recursively traverse the entire deleted tree and clean up fiber fields. - // This is more aggressive than ideal, and the long term goal is to only - // have to detach the deleted tree at the root. - detachFiberAfterEffects(fiber); - if (fiber === deletedSubtreeRoot) { - nextEffect = null; - return; - } - } else { - // This is the default branch (level 0). We do not recursively clear all - // the fiber fields. Only the root of the deleted subtree. - if (fiber === deletedSubtreeRoot) { - detachFiberAfterEffects(fiber); - nextEffect = null; - return; - } - } - - if (sibling !== null) { - sibling.return = returnFiber; - nextEffect = sibling; - return; - } - - nextEffect = returnFiber; - } -} - -function commitPassiveUnmountInsideDeletedTreeOnFiber( - current: Fiber, - nearestMountedAncestor: Fiber | null, -): void { - switch (current.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - commitHookPassiveUnmountEffects( - current, - nearestMountedAncestor, - HookPassive, - ); - break; - } - // TODO: run passive unmount effects when unmounting a root. - // Because passive unmount effects are not currently run, - // the cache instance owned by the root will never be freed. - // When effects are run, the cache should be freed here: - // case HostRoot: { - // if (enableCache) { - // const cache = current.memoizedState.cache; - // releaseCache(cache); - // } - // break; - // } - case LegacyHiddenComponent: - case OffscreenComponent: { - if (enableCache) { - if ( - current.memoizedState !== null && - current.memoizedState.cachePool !== null - ) { - const cache: Cache = current.memoizedState.cachePool.pool; - // Retain/release the cache used for pending (suspended) nodes. - // Note that this is only reached in the non-suspended/visible case: - // when the content is suspended/hidden, the retain/release occurs - // via the parent Suspense component (see case above). - if (cache != null) { - retainCache(cache); - } - } - } - break; - } - case SuspenseComponent: { - if (enableTransitionTracing) { - // We need to mark this fiber's parents as deleted - const offscreenFiber: Fiber = (current.child: any); - const instance: OffscreenInstance = offscreenFiber.stateNode; - const transitions = instance._transitions; - if (transitions !== null) { - const abortReason = { - reason: 'suspense', - name: current.memoizedProps.unstable_name || null, - }; - if ( - current.memoizedState === null || - current.memoizedState.dehydrated === null - ) { - abortParentMarkerTransitionsForDeletedFiber( - offscreenFiber, - abortReason, - transitions, - instance, - true, - ); - - if (nearestMountedAncestor !== null) { - abortParentMarkerTransitionsForDeletedFiber( - nearestMountedAncestor, - abortReason, - transitions, - instance, - false, - ); - } - } - } - } - break; - } - case CacheComponent: { - if (enableCache) { - const cache = current.memoizedState.cache; - releaseCache(cache); - } - break; - } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - // We need to mark this fiber's parents as deleted - const instance: TracingMarkerInstance = current.stateNode; - const transitions = instance.transitions; - if (transitions !== null) { - const abortReason = { - reason: 'marker', - name: current.memoizedProps.name, - }; - abortParentMarkerTransitionsForDeletedFiber( - current, - abortReason, - transitions, - null, - true, - ); - - if (nearestMountedAncestor !== null) { - abortParentMarkerTransitionsForDeletedFiber( - nearestMountedAncestor, - abortReason, - transitions, - null, - false, - ); - } - } - } - break; - } - } -} - -function invokeLayoutEffectMountInDEV(fiber: Fiber): void { - if (__DEV__) { - // We don't need to re-check StrictEffectsMode here. - // This function is only called if that check has already passed. - switch (fiber.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - try { - commitHookEffectListMount(HookLayout | HookHasEffect, fiber); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - break; - } - case ClassComponent: { - const instance = fiber.stateNode; - try { - instance.componentDidMount(); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - break; - } - } - } -} - -function invokePassiveEffectMountInDEV(fiber: Fiber): void { - if (__DEV__) { - // We don't need to re-check StrictEffectsMode here. - // This function is only called if that check has already passed. - switch (fiber.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - try { - commitHookEffectListMount(HookPassive | HookHasEffect, fiber); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - break; - } - } - } -} - -function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__) { - // We don't need to re-check StrictEffectsMode here. - // This function is only called if that check has already passed. - switch (fiber.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - try { - commitHookEffectListUnmount( - HookLayout | HookHasEffect, - fiber, - fiber.return, - ); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - break; - } - case ClassComponent: { - const instance = fiber.stateNode; - if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount(fiber, fiber.return, instance); - } - break; - } - } - } -} - -function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__) { - // We don't need to re-check StrictEffectsMode here. - // This function is only called if that check has already passed. - switch (fiber.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - try { - commitHookEffectListUnmount( - HookPassive | HookHasEffect, - fiber, - fiber.return, - ); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - } - } - } -} - -export { - commitPlacement, - commitAttachRef, - invokeLayoutEffectMountInDEV, - invokeLayoutEffectUnmountInDEV, - invokePassiveEffectMountInDEV, - invokePassiveEffectUnmountInDEV, -}; From d05c12ffd74915b857389e153ca340c76d068de4 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 2 Dec 2022 17:47:04 +0000 Subject: [PATCH 14/17] prettier --- packages/react-reconciler/src/ReactFiberBeginWork.js | 3 ++- packages/react-reconciler/src/ReactFiberCommitWork.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 65dbb3e9e0282..4faf858f3370d 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -677,7 +677,8 @@ function updateOffscreenComponent( ) { const nextProps: OffscreenProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - const isPendingDetached = (workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0; + const isPendingDetached = + (workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0; const prevState: OffscreenState | null = current !== null ? current.memoizedState : null; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index aee54674c14ca..dc9cab85232ce 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2883,7 +2883,7 @@ function commitMutationEffectsOnFiber( // to support batching of `attach` and `detach` calls. finishedWork.stateNode._visibility &= ~OffscreenDetached; finishedWork.stateNode._visibility |= - finishedWork.stateNode._pendingVisibility & OffscreenDetached; + finishedWork.stateNode._pendingVisibility & OffscreenDetached; if (flags & Visibility) { const offscreenInstance: OffscreenInstance = finishedWork.stateNode; From 20d8a041922cd875eabe71eff3b536e4c74251a5 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 5 Dec 2022 13:04:54 +0000 Subject: [PATCH 15/17] Rename variable + strongly type stateNode --- .../react-reconciler/src/ReactFiberBeginWork.js | 4 ++-- .../react-reconciler/src/ReactFiberCommitWork.js | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 4faf858f3370d..7b3f525d2dcb9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -677,7 +677,7 @@ function updateOffscreenComponent( ) { const nextProps: OffscreenProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - const isPendingDetached = + const nextIsDetached = (workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0; const prevState: OffscreenState | null = @@ -689,7 +689,7 @@ function updateOffscreenComponent( nextProps.mode === 'hidden' || (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding') || - isPendingDetached + nextIsDetached ) { // Rendering a hidden tree. diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index dc9cab85232ce..8a57bc185383f 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2876,18 +2876,19 @@ function commitMutationEffectsOnFiber( } commitReconciliationEffects(finishedWork); + + const offscreenInstance: OffscreenInstance = finishedWork.stateNode; + // TODO: Add explicit effect flag to set _current. - finishedWork.stateNode._current = finishedWork; + offscreenInstance._current = finishedWork; // Offscreen stores pending changes to visibility in `_pendingVisibility`. This is // to support batching of `attach` and `detach` calls. - finishedWork.stateNode._visibility &= ~OffscreenDetached; - finishedWork.stateNode._visibility |= - finishedWork.stateNode._pendingVisibility & OffscreenDetached; + offscreenInstance._visibility &= ~OffscreenDetached; + offscreenInstance._visibility |= + offscreenInstance._pendingVisibility & OffscreenDetached; if (flags & Visibility) { - const offscreenInstance: OffscreenInstance = finishedWork.stateNode; - // Track the current state on the Offscreen instance so we can // read it during an event if (isHidden) { From 558bd866b0515b3ad0664852a8d87a2fcc719cee Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 12 Dec 2022 12:29:23 +0000 Subject: [PATCH 16/17] Add todo --- packages/react-reconciler/src/ReactFiberCommitWork.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 8a57bc185383f..39017704c87b6 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2420,6 +2420,8 @@ export function detachOffscreenInstance(instance: OffscreenInstance): void { return; } + // TODO: There is an opportunity to optimise this by not entering commit phase + // and unmounting effects directly. const root = enqueueConcurrentRenderForLane(fiber, SyncLane); if (root !== null) { instance._pendingVisibility |= OffscreenDetached; From dad8f0c343e9fce9817c1d4047a95105625f23a0 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 12 Dec 2022 13:25:01 +0000 Subject: [PATCH 17/17] Fix merge mistake --- packages/react-reconciler/src/ReactFiberCommitWork.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 39017704c87b6..eec6e1eb0142f 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -203,7 +203,7 @@ import { import { TransitionRoot, TransitionTracingMarker, -} from './ReactFiberTracingMarkerComponent.new'; +} from './ReactFiberTracingMarkerComponent'; import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop'; import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';