From f9ae5792aa21e53e0019a26ad4336d1606bb06fc Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 9 Apr 2018 17:46:37 -0700 Subject: [PATCH] Decouple update queue from Fiber type The update queue is in need of a refactor. Recent bugfixes (#12528) have exposed some flaws in how it's modeled. Upcoming features like Suspense and [redacted] also rely on the update queue in ways that weren't anticipated in the original design. Major changes: - Instead of boolean flags for `isReplace` and `isForceUpdate`, updates have a `tag` field (like Fiber). This lowers the cost for adding new types of updates. - Render phase updates are special cased. Updates scheduled during the render phase are dropped if the work-in-progress does not commit. This is used for `getDerivedStateFrom{Props,Catch}`. - `callbackList` has been replaced with a generic effect list. Aside from callbacks, this is also used for `componentDidCatch`. --- packages/react-noop-renderer/src/ReactNoop.js | 2 +- packages/react-reconciler/src/ReactFiber.js | 2 +- .../src/ReactFiberBeginWork.js | 86 +- .../src/ReactFiberClassComponent.js | 449 ++----- .../src/ReactFiberCommitWork.js | 135 +-- .../src/ReactFiberCompleteWork.js | 27 +- .../src/ReactFiberReconciler.js | 38 +- .../src/ReactFiberScheduler.js | 61 +- .../src/ReactFiberUnwindWork.js | 35 +- .../src/ReactFiberUpdateQueue.js | 394 ------ .../react-reconciler/src/ReactUpdateQueue.js | 1066 +++++++++++++++++ .../ReactIncremental-test.internal.js | 1 + .../ReactIncrementalTriangle-test.internal.js | 2 + ...ReactIncrementalPerf-test.internal.js.snap | 8 +- packages/shared/ReactTypeOfSideEffect.js | 12 +- 15 files changed, 1317 insertions(+), 1001 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberUpdateQueue.js create mode 100644 packages/react-reconciler/src/ReactUpdateQueue.js diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 7b7bb61d0dd3c..2ee461d94895e 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -15,7 +15,7 @@ */ import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {UpdateQueue} from 'react-reconciler/src/ReactFiberUpdateQueue'; +import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; import type {ReactNodeList} from 'shared/ReactTypes'; import ReactFiberReconciler from 'react-reconciler'; import {enablePersistentReconciler} from 'shared/ReactFeatureFlags'; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 1ffa168b11a30..393cecb52763a 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -12,7 +12,7 @@ import type {TypeOfWork} from 'shared/ReactTypeOfWork'; import type {TypeOfMode} from './ReactTypeOfMode'; import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {UpdateQueue} from './ReactFiberUpdateQueue'; +import type {UpdateQueue} from './ReactUpdateQueue'; import invariant from 'fbjs/lib/invariant'; import {NoEffect} from 'shared/ReactTypeOfSideEffect'; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6de5e54877ec3..47d6b2be31e17 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -35,10 +35,12 @@ import { ContextConsumer, } from 'shared/ReactTypeOfWork'; import { + NoEffect, PerformedWork, Placement, ContentReset, Ref, + DidCapture, } from 'shared/ReactTypeOfSideEffect'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import { @@ -58,7 +60,12 @@ import { reconcileChildFibers, cloneChildFibers, } from './ReactChildFiber'; -import {processUpdateQueue} from './ReactFiberUpdateQueue'; +import { + createDeriveStateFromPropsUpdate, + enqueueRenderPhaseUpdate, + processClassUpdateQueue, + processRootUpdateQueue, +} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -105,7 +112,6 @@ export default function( const { adoptClassInstance, - callGetDerivedStateFromProps, constructClassInstance, mountClassInstance, resumeMountClassInstance, @@ -260,7 +266,11 @@ export default function( if (current === null) { if (workInProgress.stateNode === null) { // In the initial pass we might need to construct the instance. - constructClassInstance(workInProgress, workInProgress.pendingProps); + constructClassInstance( + workInProgress, + workInProgress.pendingProps, + renderExpirationTime, + ); mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; @@ -278,22 +288,11 @@ export default function( renderExpirationTime, ); } - - // We processed the update queue inside updateClassInstance. It may have - // included some errors that were dispatched during the commit phase. - // TODO: Refactor class components so this is less awkward. - let didCaptureError = false; - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - shouldUpdate = true; - didCaptureError = true; - } return finishClassComponent( current, workInProgress, shouldUpdate, hasContext, - didCaptureError, renderExpirationTime, ); } @@ -303,12 +302,14 @@ export default function( workInProgress: Fiber, shouldUpdate: boolean, hasContext: boolean, - didCaptureError: boolean, renderExpirationTime: ExpirationTime, ) { // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); + const didCaptureError = + (workInProgress.effectTag & DidCapture) !== NoEffect; + if (!shouldUpdate && !didCaptureError) { // Context providers should defer to sCU for rendering if (hasContext) { @@ -413,29 +414,15 @@ export default function( pushHostRootContext(workInProgress); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - const prevState = workInProgress.memoizedState; - const state = processUpdateQueue( - current, - workInProgress, - updateQueue, - null, - null, - renderExpirationTime, - ); - memoizeState(workInProgress, state); - updateQueue = workInProgress.updateQueue; - - let element; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - // There's an uncaught error. Unmount the whole root. - element = null; - } else if (prevState === state) { + const prevChildren = workInProgress.memoizedState; + processRootUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + const nextChildren = workInProgress.memoizedState; + + if (nextChildren === prevChildren) { // If the state is the same as before, that's a bailout because we had // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current, workInProgress); - } else { - element = state.element; } const root: FiberRoot = workInProgress.stateNode; if ( @@ -460,16 +447,15 @@ export default function( workInProgress.child = mountChildFibers( workInProgress, null, - element, + nextChildren, renderExpirationTime, ); } else { // Otherwise reset hydration state in case we aborted and resumed another // root. resetHydrationState(); - reconcileChildren(current, workInProgress, element); + reconcileChildren(current, workInProgress, nextChildren); } - memoizeState(workInProgress, state); return workInProgress.child; } resetHydrationState(); @@ -607,19 +593,16 @@ export default function( workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null; - if (typeof Component.getDerivedStateFromProps === 'function') { - const partialState = callGetDerivedStateFromProps( - workInProgress, - value, - props, - workInProgress.memoizedState, - ); - - if (partialState !== null && partialState !== undefined) { - workInProgress.memoizedState = Object.assign( - {}, - workInProgress.memoizedState, - partialState, + const getDerivedStateFromProps = Component.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createDeriveStateFromPropsUpdate(renderExpirationTime); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processClassUpdateQueue( + workInProgress, + updateQueue, + renderExpirationTime, ); } } @@ -635,7 +618,6 @@ export default function( workInProgress, true, hasContext, - false, renderExpirationTime, ); } else { @@ -1098,7 +1080,7 @@ export default function( function memoizeState(workInProgress: Fiber, nextState: any) { workInProgress.memoizedState = nextState; // Don't reset the updateQueue, in case there are pending updates. Resetting - // is handled by processUpdateQueue. + // is handled by processClassUpdateQueue. } function beginWork( diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 3f811c11088a3..75f63abf0da33 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -10,11 +10,9 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {LegacyContext} from './ReactFiberContext'; -import type {CapturedValue} from './ReactCapturedValue'; -import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect'; +import {Update, Snapshot, ForceUpdate} from 'shared/ReactTypeOfSideEffect'; import { - enableGetDerivedStateFromCatch, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, @@ -31,15 +29,20 @@ import warning from 'fbjs/lib/warning'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; import { - insertUpdateIntoFiber, - processUpdateQueue, -} from './ReactFiberUpdateQueue'; + enqueueUpdate, + enqueueRenderPhaseUpdate, + processClassUpdateQueue, + createStateUpdate, + createStateReplace, + createCallbackEffect, + createDeriveStateFromPropsUpdate, + createForceUpdate, +} from './ReactUpdateQueue'; const fakeInternalInstance = {}; const isArray = Array.isArray; let didWarnAboutStateAssignmentForComponent; -let didWarnAboutUndefinedDerivedState; let didWarnAboutUninitializedState; let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; let didWarnAboutLegacyLifecyclesAndDerivedState; @@ -47,7 +50,6 @@ let warnOnInvalidCallback; if (__DEV__) { didWarnAboutStateAssignmentForComponent = new Set(); - didWarnAboutUndefinedDerivedState = new Set(); didWarnAboutUninitializedState = new Set(); didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); @@ -92,17 +94,16 @@ if (__DEV__) { }); Object.freeze(fakeInternalInstance); } -function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array) { - const resultState = {}; - for (let i = 0; i < capturedValues.length; i++) { - const capturedValue: CapturedValue = (capturedValues[i]: any); - const error = capturedValue.value; - const partialState = ctor.getDerivedStateFromCatch.call(null, error); - if (partialState !== null && partialState !== undefined) { - Object.assign(resultState, partialState); - } + +function enqueueDerivedStateFromProps( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): void { + const getDerivedStateFromProps = workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createDeriveStateFromPropsUpdate(renderExpirationTime); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); } - return resultState; } export default function( @@ -120,64 +121,57 @@ export default function( hasContextChanged, } = legacyContext; - // Class component state updater - const updater = { + const classComponentUpdater = { isMounted, - enqueueSetState(instance, partialState, callback) { + enqueueSetState(instance, payload, callback) { const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); - } const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createStateUpdate(payload, expirationTime); + enqueueUpdate(fiber, update, expirationTime); + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(fiber, callbackUpdate, expirationTime); + } + scheduleWork(fiber, expirationTime); }, - enqueueReplaceState(instance, state, callback) { + enqueueReplaceState(instance, payload, callback) { const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'replaceState'); - } const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createStateReplace(payload, expirationTime); + enqueueUpdate(fiber, update, expirationTime); + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(fiber, callbackUpdate, expirationTime); + } + scheduleWork(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'forceUpdate'); - } const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createForceUpdate(expirationTime); + enqueueUpdate(fiber, update, expirationTime); + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(fiber, callbackUpdate, expirationTime); + } + scheduleWork(fiber, expirationTime); }, }; @@ -190,11 +184,7 @@ export default function( newState, newContext, ) { - if ( - oldProps === null || - (workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate) - ) { + if (workInProgress.effectTag & ForceUpdate) { // If the workInProgress already has an Update effect, return true return true; } @@ -420,13 +410,8 @@ export default function( } } - function resetInputPointers(workInProgress: Fiber, instance: any) { - instance.props = workInProgress.memoizedProps; - instance.state = workInProgress.memoizedState; - } - function adoptClassInstance(workInProgress: Fiber, instance: any): void { - instance.updater = updater; + instance.updater = classComponentUpdater; workInProgress.stateNode = instance; // The instance needs access to the fiber so that it can schedule updates ReactInstanceMap.set(instance, workInProgress); @@ -435,7 +420,11 @@ export default function( } } - function constructClassInstance(workInProgress: Fiber, props: any): any { + function constructClassInstance( + workInProgress: Fiber, + props: any, + renderExpirationTime: ExpirationTime, + ): any { const ctor = workInProgress.type; const unmaskedContext = getUnmaskedContext(workInProgress); const needsContext = isContextConsumer(workInProgress); @@ -453,10 +442,10 @@ export default function( } const instance = new ctor(props, context); - const state = + const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state - : null; + : null); adoptClassInstance(workInProgress, instance); if (__DEV__) { @@ -545,26 +534,6 @@ export default function( } } - workInProgress.memoizedState = state; - - const partialState = callGetDerivedStateFromProps( - workInProgress, - instance, - props, - state, - ); - - if (partialState !== null && partialState !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - workInProgress.memoizedState = Object.assign( - {}, - workInProgress.memoizedState, - partialState, - ); - } - // Cache unmasked context so we can avoid recreating masked context unless necessary. // ReactFiberContext usually updates this cache but can't for newly-created instances. if (needsContext) { @@ -597,7 +566,7 @@ export default function( getComponentName(workInProgress) || 'Component', ); } - updater.enqueueReplaceState(instance, instance.state, null); + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } } @@ -631,50 +600,7 @@ export default function( ); } } - updater.enqueueReplaceState(instance, instance.state, null); - } - } - - function callGetDerivedStateFromProps( - workInProgress: Fiber, - instance: any, - nextProps: any, - prevState: any, - ) { - const {type} = workInProgress; - - if (typeof type.getDerivedStateFromProps === 'function') { - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - // Invoke method an extra time to help detect side-effects. - type.getDerivedStateFromProps.call(null, nextProps, prevState); - } - - const partialState = type.getDerivedStateFromProps.call( - null, - nextProps, - prevState, - ); - - if (__DEV__) { - if (partialState === undefined) { - const componentName = getComponentName(workInProgress) || 'Component'; - if (!didWarnAboutUndefinedDerivedState.has(componentName)) { - didWarnAboutUndefinedDerivedState.add(componentName); - warning( - false, - '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + - 'You have returned undefined.', - componentName, - ); - } - } - } - - return partialState; + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } } @@ -684,7 +610,6 @@ export default function( renderExpirationTime: ExpirationTime, ): void { const ctor = workInProgress.type; - const current = workInProgress.alternate; if (__DEV__) { checkClassInstance(workInProgress); @@ -715,6 +640,17 @@ export default function( } } + enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processClassUpdateQueue( + workInProgress, + updateQueue, + renderExpirationTime, + ); + instance.state = workInProgress.memoizedState; + } + // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( @@ -726,18 +662,17 @@ export default function( callComponentWillMount(workInProgress, instance); // If we had additional state updates during this life-cycle, let's // process them now. - const updateQueue = workInProgress.updateQueue; + updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - instance.state = processUpdateQueue( - current, + processClassUpdateQueue( workInProgress, updateQueue, - instance, - props, renderExpirationTime, ); + instance.state = workInProgress.memoizedState; } } + if (typeof instance.componentDidMount === 'function') { workInProgress.effectTag |= Update; } @@ -749,10 +684,11 @@ export default function( ): boolean { const ctor = workInProgress.type; const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); const oldProps = workInProgress.memoizedProps; const newProps = workInProgress.pendingProps; + instance.props = oldProps; + const oldContext = instance.context; const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); @@ -782,103 +718,28 @@ export default function( } } - // Compute the next state using the memoized state and the update queue. - const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - let derivedStateFromCatch; - if (workInProgress.updateQueue !== null) { - newState = processUpdateQueue( - null, - workInProgress, - workInProgress.updateQueue, - instance, - newProps, - renderExpirationTime, - ); - - let updateQueue = workInProgress.updateQueue; - if ( - updateQueue !== null && - updateQueue.capturedValues !== null && - (enableGetDerivedStateFromCatch && - typeof ctor.getDerivedStateFromCatch === 'function') - ) { - const capturedValues = updateQueue.capturedValues; - // Don't remove these from the update queue yet. We need them in - // finishClassComponent. Do the reset there. - // TODO: This is awkward. Refactor class components. - // updateQueue.capturedValues = null; - derivedStateFromCatch = callGetDerivedStateFromCatch( - ctor, - capturedValues, - ); - } - } else { - newState = oldState; + // Only call getDerivedStateFromProps if the props have changed + if (oldProps !== newProps) { + enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); } - let derivedStateFromProps; - if (oldProps !== newProps) { - // The prevState parameter should be the partially updated state. - // Otherwise, spreading state in return values could override updates. - derivedStateFromProps = callGetDerivedStateFromProps( + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processClassUpdateQueue( workInProgress, - instance, - newProps, - newState, + updateQueue, + renderExpirationTime, ); - } - - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromProps - : Object.assign({}, newState, derivedStateFromProps); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromProps, - ); - } - } - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromCatch - : Object.assign({}, newState, derivedStateFromCatch); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromCatch, - ); - } + newState = workInProgress.memoizedState; } if ( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !(workInProgress.effectTag & ForceUpdate) ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. @@ -925,9 +786,9 @@ export default function( } // If shouldComponentUpdate returned false, we should still update the - // memoized props/state to indicate that this work can be reused. - memoizeProps(workInProgress, newProps); - memoizeState(workInProgress, newState); + // memoized state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; } // Update the existing instance's state, props, and context pointers even @@ -947,10 +808,11 @@ export default function( ): boolean { const ctor = workInProgress.type; const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); const oldProps = workInProgress.memoizedProps; const newProps = workInProgress.pendingProps; + instance.props = oldProps; + const oldContext = instance.context; const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); @@ -980,104 +842,28 @@ export default function( } } - // Compute the next state using the memoized state and the update queue. - const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - let derivedStateFromCatch; - - if (workInProgress.updateQueue !== null) { - newState = processUpdateQueue( - current, - workInProgress, - workInProgress.updateQueue, - instance, - newProps, - renderExpirationTime, - ); - - let updateQueue = workInProgress.updateQueue; - if ( - updateQueue !== null && - updateQueue.capturedValues !== null && - (enableGetDerivedStateFromCatch && - typeof ctor.getDerivedStateFromCatch === 'function') - ) { - const capturedValues = updateQueue.capturedValues; - // Don't remove these from the update queue yet. We need them in - // finishClassComponent. Do the reset there. - // TODO: This is awkward. Refactor class components. - // updateQueue.capturedValues = null; - derivedStateFromCatch = callGetDerivedStateFromCatch( - ctor, - capturedValues, - ); - } - } else { - newState = oldState; + // Only call getDerivedStateFromProps if the props have changed + if (oldProps !== newProps) { + enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); } - let derivedStateFromProps; - if (oldProps !== newProps) { - // The prevState parameter should be the partially updated state. - // Otherwise, spreading state in return values could override updates. - derivedStateFromProps = callGetDerivedStateFromProps( + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processClassUpdateQueue( workInProgress, - instance, - newProps, - newState, + updateQueue, + renderExpirationTime, ); - } - - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromProps - : Object.assign({}, newState, derivedStateFromProps); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromProps, - ); - } - } - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromCatch - : Object.assign({}, newState, derivedStateFromCatch); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromCatch, - ); - } + newState = workInProgress.memoizedState; } if ( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !(workInProgress.effectTag & ForceUpdate) ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. @@ -1154,8 +940,8 @@ export default function( // If shouldComponentUpdate returned false, we should still update the // memoized props/state to indicate that this work can be reused. - memoizeProps(workInProgress, newProps); - memoizeState(workInProgress, newState); + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; } // Update the existing instance's state, props, and context pointers even @@ -1169,7 +955,6 @@ export default function( return { adoptClassInstance, - callGetDerivedStateFromProps, constructClassInstance, mountClassInstance, resumeMountClassInstance, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index afa5b46d0b0f8..75e1f3691de7b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -11,7 +11,7 @@ import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {CapturedValue, CapturedError} from './ReactCapturedValue'; +import type {UpdateQueueMethods} from './ReactUpdateQueue'; import { enableMutatingReconciler, @@ -36,10 +36,8 @@ import { import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; -import {commitCallbacks} from './ReactFiberUpdateQueue'; import {onCommitUnmount} from './ReactFiberDevToolsHook'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; -import {logCapturedError} from './ReactFiberErrorLogger'; import getComponentName from 'shared/getComponentName'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; @@ -54,44 +52,9 @@ if (__DEV__) { didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); } -function logError(boundary: Fiber, errorInfo: CapturedValue) { - const source = errorInfo.source; - let stack = errorInfo.stack; - if (stack === null) { - stack = getStackAddendumByWorkInProgressFiber(source); - } - - const capturedError: CapturedError = { - componentName: source !== null ? getComponentName(source) : null, - componentStack: stack !== null ? stack : '', - error: errorInfo.value, - errorBoundary: null, - errorBoundaryName: null, - errorBoundaryFound: false, - willRetry: false, - }; - - if (boundary !== null && boundary.tag === ClassComponent) { - capturedError.errorBoundary = boundary.stateNode; - capturedError.errorBoundaryName = getComponentName(boundary); - capturedError.errorBoundaryFound = true; - capturedError.willRetry = true; - } - - try { - logCapturedError(capturedError); - } catch (e) { - // Prevent cycle if logCapturedError() throws. - // A cycle may still occur if logCapturedError renders a component that throws. - const suppressLogging = e && e.suppressReactErrorLogging; - if (!suppressLogging) { - console.error(e); - } - } -} - export default function( config: HostConfig, + updateQueueMethods: UpdateQueueMethods, captureError: (failedFiber: Fiber, error: mixed) => Fiber | null, scheduleWork: ( fiber: Fiber, @@ -107,6 +70,8 @@ export default function( ) { const {getPublicInstance, mutation, persistence} = config; + const {commitClassUpdateQueue, commitRootUpdateQueue} = updateQueueMethods; + const callComponentWillUnmountWithTimer = function(current, instance) { startPhaseTimer(current, 'componentWillUnmount'); instance.props = current.memoizedProps; @@ -251,25 +216,22 @@ export default function( } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitCallbacks(updateQueue, instance); + commitClassUpdateQueue( + finishedWork, + updateQueue, + committedExpirationTime, + ); } return; } case HostRoot: { const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - let instance = null; - if (finishedWork.child !== null) { - switch (finishedWork.child.tag) { - case HostComponent: - instance = getPublicInstance(finishedWork.child.stateNode); - break; - case ClassComponent: - instance = finishedWork.child.stateNode; - break; - } - } - commitCallbacks(updateQueue, instance); + commitRootUpdateQueue( + finishedWork, + updateQueue, + committedExpirationTime, + ); } return; } @@ -306,73 +268,6 @@ export default function( } } - function commitErrorLogging( - finishedWork: Fiber, - onUncaughtError: (error: Error) => void, - ) { - switch (finishedWork.tag) { - case ClassComponent: - { - const ctor = finishedWork.type; - const instance = finishedWork.stateNode; - const updateQueue = finishedWork.updateQueue; - invariant( - updateQueue !== null && updateQueue.capturedValues !== null, - 'An error logging effect should not have been scheduled if no errors ' + - 'were captured. This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); - const capturedErrors = updateQueue.capturedValues; - updateQueue.capturedValues = null; - - if (typeof ctor.getDerivedStateFromCatch !== 'function') { - // To preserve the preexisting retry behavior of error boundaries, - // we keep track of which ones already failed during this batch. - // This gets reset before we yield back to the browser. - // TODO: Warn in strict mode if getDerivedStateFromCatch is - // not defined. - markLegacyErrorBoundaryAsFailed(instance); - } - - instance.props = finishedWork.memoizedProps; - instance.state = finishedWork.memoizedState; - for (let i = 0; i < capturedErrors.length; i++) { - const errorInfo = capturedErrors[i]; - const error = errorInfo.value; - const stack = errorInfo.stack; - logError(finishedWork, errorInfo); - instance.componentDidCatch(error, { - componentStack: stack !== null ? stack : '', - }); - } - } - break; - case HostRoot: { - const updateQueue = finishedWork.updateQueue; - invariant( - updateQueue !== null && updateQueue.capturedValues !== null, - 'An error logging effect should not have been scheduled if no errors ' + - 'were captured. This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); - const capturedErrors = updateQueue.capturedValues; - updateQueue.capturedValues = null; - for (let i = 0; i < capturedErrors.length; i++) { - const errorInfo = capturedErrors[i]; - logError(finishedWork, errorInfo); - onUncaughtError(errorInfo.value); - } - break; - } - default: - invariant( - false, - 'This unit of work tag cannot capture errors. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - } - } - function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { @@ -564,7 +459,6 @@ export default function( }, commitLifeCycles, commitBeforeMutationLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, }; @@ -892,7 +786,6 @@ export default function( commitDeletion, commitWork, commitLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, }; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 02779db8b6561..aec3bc17c6f24 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -38,13 +38,7 @@ import { Fragment, Mode, } from 'shared/ReactTypeOfWork'; -import { - Placement, - Ref, - Update, - ErrLog, - DidCapture, -} from 'shared/ReactTypeOfSideEffect'; +import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'fbjs/lib/invariant'; import {reconcileChildFibers} from './ReactChildFiber'; @@ -416,20 +410,6 @@ export default function( case ClassComponent: { // We are leaving this subtree, so pop context if any. popLegacyContextProvider(workInProgress); - - // If this component caught an error, schedule an error log effect. - const instance = workInProgress.stateNode; - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - workInProgress.effectTag &= ~DidCapture; - if (typeof instance.componentDidCatch === 'function') { - workInProgress.effectTag |= ErrLog; - } else { - // Normally we clear this in the commit phase, but since we did not - // schedule an effect, we need to reset it here. - updateQueue.capturedValues = null; - } - } return null; } case HostRoot: { @@ -449,11 +429,6 @@ export default function( workInProgress.effectTag &= ~Placement; } updateHostContainer(workInProgress); - - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - workInProgress.effectTag |= ErrLog; - } return null; } case HostComponent: { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 8118675232b37..b9aa3a5c971b8 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,7 +26,11 @@ import warning from 'fbjs/lib/warning'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import ReactFiberScheduler from './ReactFiberScheduler'; -import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue'; +import { + createStateReplace, + createCallbackEffect, + enqueueUpdate, +} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -339,28 +343,24 @@ export default function( } } + const update = createStateReplace(element, expirationTime); + enqueueUpdate(current, update, expirationTime); + callback = callback === undefined ? null : callback; - if (__DEV__) { - warning( - callback === null || typeof callback === 'function', - 'render(...): Expected the last optional `callback` argument to be a ' + - 'function. Instead received: %s.', - callback, - ); + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warning( + callback === null || typeof callback === 'function', + 'render(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callback, + ); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(current, callbackUpdate, expirationTime); } - const update = { - expirationTime, - partialState: {element}, - callback, - isReplace: false, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(current, update); scheduleWork(current, expirationTime); - return expirationTime; } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 46cef6cec6c02..29ad58ba86dcc 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -26,12 +26,11 @@ import { PlacementAndUpdate, Deletion, ContentReset, - Callback, - DidCapture, + UpdateQueue as UpdateQueueEffect, + ShouldCapture, Ref, Incomplete, HostEffectMask, - ErrLog, } from 'shared/ReactTypeOfSideEffect'; import { HostRoot, @@ -89,10 +88,10 @@ import { import {AsyncMode} from './ReactTypeOfMode'; import ReactFiberLegacyContext from './ReactFiberContext'; import ReactFiberNewContext from './ReactFiberNewContext'; -import { - getUpdateExpirationTime, - insertUpdateIntoFiber, -} from './ReactFiberUpdateQueue'; +import ReactUpdateQueue, { + createCatchUpdate, + enqueueUpdate, +} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; @@ -202,6 +201,11 @@ export default function( scheduleWork, isAlreadyFailedLegacyErrorBoundary, ); + const updateQueueMethods = ReactUpdateQueue( + config, + markLegacyErrorBoundaryAsFailed, + onUncaughtError, + ); const { commitBeforeMutationLifeCycles, commitResetTextContent, @@ -209,11 +213,11 @@ export default function( commitDeletion, commitWork, commitLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, } = ReactFiberCommitWork( config, + updateQueueMethods, onCommitPhaseError, scheduleWork, computeExpirationForFiber, @@ -435,7 +439,7 @@ export default function( while (nextEffect !== null) { const effectTag = nextEffect.effectTag; - if (effectTag & (Update | Callback)) { + if (effectTag & (Update | UpdateQueueEffect)) { recordEffect(); const current = nextEffect.alternate; commitLifeCycles( @@ -447,10 +451,6 @@ export default function( ); } - if (effectTag & ErrLog) { - commitErrorLogging(nextEffect, onUncaughtError); - } - if (effectTag & Ref) { recordEffect(); commitAttachRef(nextEffect); @@ -503,6 +503,8 @@ export default function( ); root.pendingCommitExpirationTime = NoWork; + // console.log('commitRoot', committedExpirationTime); + const currentTime = recalculateCurrentTime(); // Reset this to null before calling lifecycles @@ -681,7 +683,16 @@ export default function( } // Check for pending updates. - let newExpirationTime = getUpdateExpirationTime(workInProgress); + let newExpirationTime = NoWork; + switch (workInProgress.tag) { + case HostRoot: + case ClassComponent: { + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + newExpirationTime = updateQueue.expirationTime; + } + } + } // TODO: Calls need to visit stateNode @@ -799,7 +810,7 @@ export default function( // capture values if possible. const next = unwindWork(workInProgress); // Because this fiber did not complete, don't reset its expiration time. - if (workInProgress.effectTag & DidCapture) { + if (workInProgress.effectTag & ShouldCapture) { // Restarting an error boundary stopFailedWorkTimer(workInProgress); } else { @@ -974,7 +985,12 @@ export default function( onUncaughtError(thrownValue); break; } - throwException(returnFiber, sourceFiber, thrownValue); + throwException( + returnFiber, + sourceFiber, + thrownValue, + nextRenderExpirationTime, + ); nextUnitOfWork = completeUnitOfWork(sourceFiber); } break; @@ -1023,18 +1039,9 @@ export default function( } function scheduleCapture(sourceFiber, boundaryFiber, value, expirationTime) { - // TODO: We only support dispatching errors. const capturedValue = createCapturedValue(value, sourceFiber); - const update = { - expirationTime, - partialState: null, - callback: null, - isReplace: false, - isForced: false, - capturedValue, - next: null, - }; - insertUpdateIntoFiber(boundaryFiber, update); + const update = createCatchUpdate(capturedValue, expirationTime); + enqueueUpdate(boundaryFiber, update, expirationTime); scheduleWork(boundaryFiber, expirationTime); } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 6565e4888df1b..4680f158e4e52 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -12,10 +12,9 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; import type {LegacyContext} from './ReactFiberContext'; import type {NewContext} from './ReactFiberNewContext'; -import type {UpdateQueue} from './ReactFiberUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {ensureUpdateQueues} from './ReactFiberUpdateQueue'; +import {enqueueRenderPhaseUpdate, createCatchUpdate} from './ReactUpdateQueue'; import { ClassComponent, @@ -55,6 +54,7 @@ export default function( returnFiber: Fiber, sourceFiber: Fiber, rawValue: mixed, + renderExpirationTime: ExpirationTime, ) { // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; @@ -69,16 +69,18 @@ export default function( case HostRoot: { // Uncaught error const errorInfo = value; - ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue< - any, - > = (workInProgress.updateQueue: any); - updateQueue.capturedValues = [errorInfo]; workInProgress.effectTag |= ShouldCapture; + const update = createCatchUpdate(errorInfo, renderExpirationTime); + enqueueRenderPhaseUpdate( + workInProgress, + update, + renderExpirationTime, + ); return; } case ClassComponent: // Capture and retry + const errorInfo = value; const ctor = workInProgress.type; const instance = workInProgress.stateNode; if ( @@ -89,17 +91,15 @@ export default function( typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance))) ) { - ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue< - any, - > = (workInProgress.updateQueue: any); - const capturedValues = updateQueue.capturedValues; - if (capturedValues === null) { - updateQueue.capturedValues = [value]; - } else { - capturedValues.push(value); - } workInProgress.effectTag |= ShouldCapture; + + // Schedule the error boundary to re-render using updated state + const update = createCatchUpdate(errorInfo, renderExpirationTime); + enqueueRenderPhaseUpdate( + workInProgress, + update, + renderExpirationTime, + ); return; } break; @@ -116,7 +116,6 @@ export default function( popLegacyContextProvider(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { - workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; return workInProgress; } return null; diff --git a/packages/react-reconciler/src/ReactFiberUpdateQueue.js b/packages/react-reconciler/src/ReactFiberUpdateQueue.js deleted file mode 100644 index df66807dce24a..0000000000000 --- a/packages/react-reconciler/src/ReactFiberUpdateQueue.js +++ /dev/null @@ -1,394 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {Fiber} from './ReactFiber'; -import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {CapturedValue} from './ReactCapturedValue'; - -import { - debugRenderPhaseSideEffects, - debugRenderPhaseSideEffectsForStrictMode, -} from 'shared/ReactFeatureFlags'; -import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect'; -import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; -import invariant from 'fbjs/lib/invariant'; -import warning from 'fbjs/lib/warning'; -import {StrictMode} from './ReactTypeOfMode'; - -import {NoWork} from './ReactFiberExpirationTime'; - -let didWarnUpdateInsideUpdate; - -if (__DEV__) { - didWarnUpdateInsideUpdate = false; -} - -type PartialState = - | $Subtype - | ((prevState: State, props: Props) => $Subtype); - -// Callbacks are not validated until invocation -type Callback = mixed; - -export type Update = { - expirationTime: ExpirationTime, - partialState: PartialState, - callback: Callback | null, - isReplace: boolean, - isForced: boolean, - capturedValue: CapturedValue | null, - next: Update | null, -}; - -// Singly linked-list of updates. When an update is scheduled, it is added to -// the queue of the current fiber and the work-in-progress fiber. The two queues -// are separate but they share a persistent structure. -// -// During reconciliation, updates are removed from the work-in-progress fiber, -// but they remain on the current fiber. That ensures that if a work-in-progress -// is aborted, the aborted updates are recovered by cloning from current. -// -// The work-in-progress queue is always a subset of the current queue. -// -// When the tree is committed, the work-in-progress becomes the current. -export type UpdateQueue = { - // A processed update is not removed from the queue if there are any - // unprocessed updates that came before it. In that case, we need to keep - // track of the base state, which represents the base state of the first - // unprocessed update, which is the same as the first update in the list. - baseState: State, - // For the same reason, we keep track of the remaining expiration time. - expirationTime: ExpirationTime, - first: Update | null, - last: Update | null, - callbackList: Array> | null, - hasForceUpdate: boolean, - isInitialized: boolean, - capturedValues: Array> | null, - - // Dev only - isProcessing?: boolean, -}; - -function createUpdateQueue(baseState: State): UpdateQueue { - const queue: UpdateQueue = { - baseState, - expirationTime: NoWork, - first: null, - last: null, - callbackList: null, - hasForceUpdate: false, - isInitialized: false, - capturedValues: null, - }; - if (__DEV__) { - queue.isProcessing = false; - } - return queue; -} - -export function insertUpdateIntoQueue( - queue: UpdateQueue, - update: Update, -): void { - // Append the update to the end of the list. - if (queue.last === null) { - // Queue is empty - queue.first = queue.last = update; - } else { - queue.last.next = update; - queue.last = update; - } - if ( - queue.expirationTime === NoWork || - queue.expirationTime > update.expirationTime - ) { - queue.expirationTime = update.expirationTime; - } -} - -let q1; -let q2; -export function ensureUpdateQueues(fiber: Fiber) { - q1 = q2 = null; - // We'll have at least one and at most two distinct update queues. - const alternateFiber = fiber.alternate; - let queue1 = fiber.updateQueue; - if (queue1 === null) { - // TODO: We don't know what the base state will be until we begin work. - // It depends on which fiber is the next current. Initialize with an empty - // base state, then set to the memoizedState when rendering. Not super - // happy with this approach. - queue1 = fiber.updateQueue = createUpdateQueue((null: any)); - } - - let queue2; - if (alternateFiber !== null) { - queue2 = alternateFiber.updateQueue; - if (queue2 === null) { - queue2 = alternateFiber.updateQueue = createUpdateQueue((null: any)); - } - } else { - queue2 = null; - } - queue2 = queue2 !== queue1 ? queue2 : null; - - // Use module variables instead of returning a tuple - q1 = queue1; - q2 = queue2; -} - -export function insertUpdateIntoFiber( - fiber: Fiber, - update: Update, -): void { - ensureUpdateQueues(fiber); - const queue1: Fiber = (q1: any); - const queue2: Fiber | null = (q2: any); - - // Warn if an update is scheduled from inside an updater function. - if (__DEV__) { - if ( - (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && - !didWarnUpdateInsideUpdate - ) { - warning( - false, - 'An update (setState, replaceState, or forceUpdate) was scheduled ' + - 'from inside an update function. Update functions should be pure, ' + - 'with zero side-effects. Consider using componentDidUpdate or a ' + - 'callback.', - ); - didWarnUpdateInsideUpdate = true; - } - } - - // If there's only one queue, add the update to that queue and exit. - if (queue2 === null) { - insertUpdateIntoQueue(queue1, update); - return; - } - - // If either queue is empty, we need to add to both queues. - if (queue1.last === null || queue2.last === null) { - insertUpdateIntoQueue(queue1, update); - insertUpdateIntoQueue(queue2, update); - return; - } - - // If both lists are not empty, the last update is the same for both lists - // because of structural sharing. So, we should only append to one of - // the lists. - insertUpdateIntoQueue(queue1, update); - // But we still need to update the `last` pointer of queue2. - queue2.last = update; -} - -export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { - switch (fiber.tag) { - case HostRoot: - case ClassComponent: - const updateQueue = fiber.updateQueue; - if (updateQueue === null) { - return NoWork; - } - return updateQueue.expirationTime; - default: - return NoWork; - } -} - -function getStateFromUpdate(update, instance, prevState, props) { - const partialState = update.partialState; - if (typeof partialState === 'function') { - return partialState.call(instance, prevState, props); - } else { - return partialState; - } -} - -export function processUpdateQueue( - current: Fiber | null, - workInProgress: Fiber, - queue: UpdateQueue, - instance: any, - props: any, - renderExpirationTime: ExpirationTime, -): State { - if (current !== null && current.updateQueue === queue) { - // We need to create a work-in-progress queue, by cloning the current queue. - const currentQueue = queue; - queue = workInProgress.updateQueue = { - baseState: currentQueue.baseState, - expirationTime: currentQueue.expirationTime, - first: currentQueue.first, - last: currentQueue.last, - isInitialized: currentQueue.isInitialized, - capturedValues: currentQueue.capturedValues, - // These fields are no longer valid because they were already committed. - // Reset them. - callbackList: null, - hasForceUpdate: false, - }; - } - - if (__DEV__) { - // Set this flag so we can warn if setState is called inside the update - // function of another setState. - queue.isProcessing = true; - } - - // Reset the remaining expiration time. If we skip over any updates, we'll - // increase this accordingly. - queue.expirationTime = NoWork; - - // TODO: We don't know what the base state will be until we begin work. - // It depends on which fiber is the next current. Initialize with an empty - // base state, then set to the memoizedState when rendering. Not super - // happy with this approach. - let state; - if (queue.isInitialized) { - state = queue.baseState; - } else { - state = queue.baseState = workInProgress.memoizedState; - queue.isInitialized = true; - } - let dontMutatePrevState = true; - let update = queue.first; - let didSkip = false; - while (update !== null) { - const updateExpirationTime = update.expirationTime; - if (updateExpirationTime > renderExpirationTime) { - // This update does not have sufficient priority. Skip it. - const remainingExpirationTime = queue.expirationTime; - if ( - remainingExpirationTime === NoWork || - remainingExpirationTime > updateExpirationTime - ) { - // Update the remaining expiration time. - queue.expirationTime = updateExpirationTime; - } - if (!didSkip) { - didSkip = true; - queue.baseState = state; - } - // Continue to the next update. - update = update.next; - continue; - } - - // This update does have sufficient priority. - - // If no previous updates were skipped, drop this update from the queue by - // advancing the head of the list. - if (!didSkip) { - queue.first = update.next; - if (queue.first === null) { - queue.last = null; - } - } - - // Invoke setState callback an extra time to help detect side-effects. - // Ignore the return value in this case. - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - getStateFromUpdate(update, instance, state, props); - } - - // Process the update - let partialState; - if (update.isReplace) { - state = getStateFromUpdate(update, instance, state, props); - dontMutatePrevState = true; - } else { - partialState = getStateFromUpdate(update, instance, state, props); - if (partialState) { - if (dontMutatePrevState) { - // $FlowFixMe: Idk how to type this properly. - state = Object.assign({}, state, partialState); - } else { - state = Object.assign(state, partialState); - } - dontMutatePrevState = false; - } - } - if (update.isForced) { - queue.hasForceUpdate = true; - } - if (update.callback !== null) { - // Append to list of callbacks. - let callbackList = queue.callbackList; - if (callbackList === null) { - callbackList = queue.callbackList = []; - } - callbackList.push(update); - } - if (update.capturedValue !== null) { - let capturedValues = queue.capturedValues; - if (capturedValues === null) { - queue.capturedValues = [update.capturedValue]; - } else { - capturedValues.push(update.capturedValue); - } - } - update = update.next; - } - - if (queue.callbackList !== null) { - workInProgress.effectTag |= CallbackEffect; - } else if ( - queue.first === null && - !queue.hasForceUpdate && - queue.capturedValues === null - ) { - // The queue is empty. We can reset it. - workInProgress.updateQueue = null; - } - - if (!didSkip) { - didSkip = true; - queue.baseState = state; - } - - if (__DEV__) { - // No longer processing. - queue.isProcessing = false; - } - - return state; -} - -export function commitCallbacks( - queue: UpdateQueue, - context: any, -) { - const callbackList = queue.callbackList; - if (callbackList === null) { - return; - } - // Set the list to null to make sure they don't get called more than once. - queue.callbackList = null; - for (let i = 0; i < callbackList.length; i++) { - const update = callbackList[i]; - const callback = update.callback; - // This update might be processed again. Clear the callback so it's only - // called once. - update.callback = null; - invariant( - typeof callback === 'function', - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: %s', - callback, - ); - callback.call(context); - } -} diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js new file mode 100644 index 0000000000000..15a57f760ba19 --- /dev/null +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -0,0 +1,1066 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// UpdateQueue is a linked list of prioritized updates. +// +// Like fibers, update queues come in pairs: a current queue, which represents +// the visible state of the screen, and a work-in-progress queue, which is +// can be mutated and processed asynchronously before it is committed — a form +// of double buffering. If a work-in-progress render is discarded before +// finishing, we create a new work-in-progress by cloning the current queue. +// +// Both queues share a persistent, singly-linked list structure. To schedule an +// update, we append it to the end of both queues. Each queue maintains a +// pointer to first update in the persistent list that hasn't been processed. +// The work-in-progress pointer always has a position equal to or greater than +// the current queue, since we always work on that one. The current queue's +// pointer is only updated during the commit phase, when we swap in the +// work-in-progress. +// +// For example: +// +// Current pointer: A - B - C - D - E - F +// Work-in-progress pointer: D - E - F +// ^ +// The work-in-progress queue has +// processed more updates than current. +// +// The reason we append to both queues is because otherwise we might drop +// updates without ever processing them. For example, if we only add updates to +// the work-in-progress queue, some updates could be lost whenever a work-in +// -progress render restarts by cloning from current. Similarly, if we only add +// updates to the current queue, the updates will be lost whenever an already +// in-progress queue commits and swaps with the current queue. However, by +// adding to both queues, we guarantee that the update will be part of the next +// work-in-progress. (And because the work-in-progress queue becomes the +// current queue once it commits, there's no danger of applying the same +// update twice.) +// +// Prioritization +// -------------- +// +// Updates are not sorted by priority, but by insertion; new updates are always +// appended to the end of the list. +// +// The priority is still important, though. When processing the update queue +// during the render phase, only the updates with sufficient priority are +// included in the result. If we skip an update because it has insufficient +// priority, it remains in the queue to be processed later, during a lower +// priority render. Crucially, all updates subsequent to a skipped update also +// remain in the queue *regardless of their priority*. That means high priority +// updates are sometimes processed twice, at two separate priorities. We also +// keep track of a base state, that represents the state before the first +// update in the queue is applied. +// +// For example: +// +// Given a base state of '', and the following queue of updates +// +// A1 - B2 - C1 - D2 +// +// where the number indicates the priority, and the update is applied to the +// previous state by appending a letter, React will process these updates as +// two separate renders, one per distinct priority level: +// +// First render, at priority 1: +// Base state: '' +// Updates: [A1, C1] +// Result state: 'AC' +// +// Second render, at priority 2: +// Base state: 'A' <- The base state does not include C1, +// because B2 was skipped. +// Updates: [B2, C1, D2] <- C1 was rebased on top of B2 +// Result state: 'ABCD' +// +// Because we process updates in insertion order, and rebase high priority +// updates when preceding updates are skipped, the final result is deterministic +// regardless of priority. Intermediate state may vary according to system +// resources, but the final state is always the same. +// +// Render phase updates +// -------------------- +// +// A render phase update is one triggered during the render phase, while working +// on a work-in-progress tree. Our typical strategy of adding the update to both +// queues won't work, because if the work-in-progress is thrown out and +// restarted, we'll get duplicate updates. Instead, we only add render phase +// updates to the work-in-progress queue. +// +// Because normal updates are added to a persistent list that is shared between +// both queues, render phase updates go in a special list that only belongs to +// a single queue. This an artifact of structural sharing. If we instead +// implemented each queue as separate lists, we would append render phase +// updates to the end of the work-in-progress list. +// +// Examples of render phase updates: +// - getDerivedStateFromProps +// - getDerivedStateFromCatch +// - [future] loading state + +import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {CapturedValue, CapturedError} from './ReactCapturedValue'; +import type {ReactNodeList} from 'shared/ReactTypes'; + +import { + enableGetDerivedStateFromCatch, + debugRenderPhaseSideEffects, + debugRenderPhaseSideEffectsForStrictMode, +} from 'shared/ReactFeatureFlags'; +import {NoWork} from './ReactFiberExpirationTime'; +import { + UpdateQueue as UpdateQueueEffect, + ForceUpdate as ForceUpdateEffect, + ShouldCapture, + DidCapture, +} from 'shared/ReactTypeOfSideEffect'; +import {StrictMode} from './ReactTypeOfMode'; +import {ClassComponent, HostComponent} from 'shared/ReactTypeOfWork'; +import getComponentName from 'shared/getComponentName'; +import {logCapturedError} from './ReactFiberErrorLogger'; +import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; + +import invariant from 'fbjs/lib/invariant'; +import warning from 'fbjs/lib/warning'; + +// An empty update. A no-op. Used for an effect update that already committed, +// to prevent it from firing multiple times. +const NoOp = 0; +// Used for updates that do not depend on the previous value. +const ReplaceState = 1; +// Used for updates that do depend on the previous value. +const UpdateState = 2; +// forceUpdate +const ForceUpdate = 3; +// getDerivedStateFromProps +const DeriveStateFromPropsUpdate = 4; +// Error handling +const CaptureError = 5; +// Error logging +const CaptureAndLogError = 6; +// Callbacks +const Callback = 7; + +const ClassUpdateQueue = 0; +const RootUpdateQueue = 1; + +type UpdateShared = { + payload: Payload, + expirationTime: ExpirationTime, + next: U | null, + nextEffect: U | null, +}; + +type ClassUpdate = + | ({tag: 0} & UpdateShared>) + | ({tag: 1} & UpdateShared< + $Shape | ((State, Props) => $Shape | null | void), + ClassUpdate, + >) + | ({tag: 2} & UpdateShared< + State | ((State, Props) => State | null | void), + ClassUpdate, + >) + | ({tag: 3} & UpdateShared>) + | ({tag: 4} & UpdateShared>) + | ({tag: 5} & UpdateShared>) + | ({tag: 6} & UpdateShared>) + | ({tag: 7} & UpdateShared>); + +type RootUpdate = + | ({tag: 0} & UpdateShared) + | ({tag: 1} & UpdateShared) + | ({tag: 5} & UpdateShared, RootUpdate>) + | ({tag: 6} & UpdateShared, RootUpdate>) + | ({tag: 7} & UpdateShared<() => mixed, RootUpdate>); + +type UpdateQueueShared = { + expirationTime: ExpirationTime, + baseState: S, + + firstUpdate: U | null, + lastUpdate: U | null, + + firstRenderPhaseUpdate: U | null, + lastRenderPhaseUpdate: U | null, + + firstEffect: U | null, + lastEffect: U | null, + + // DEV_only + isProcessing?: boolean, +}; + +type ClassUpdateQueueType = UpdateQueueShared< + ClassUpdate, + State, +>; + +type RootUpdateQueueType = UpdateQueueShared; + +type UpdateQueueOwner = { + alternate: UpdateQueueOwner | null, + memoizedState: State, +}; + +let warnOnUndefinedDerivedState; +let didWarnUpdateInsideUpdate; +let didWarnAboutUndefinedDerivedState; +if (__DEV__) { + didWarnUpdateInsideUpdate = false; + didWarnAboutUndefinedDerivedState = new Set(); + + warnOnUndefinedDerivedState = function(workInProgress, partialState) { + if (partialState === undefined) { + const componentName = getComponentName(workInProgress) || 'Component'; + if (!didWarnAboutUndefinedDerivedState.has(componentName)) { + didWarnAboutUndefinedDerivedState.add(componentName); + warning( + false, + '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + + 'You have returned undefined.', + componentName, + ); + } + } + }; +} + +function createUpdateQueue(baseState) { + const queue = { + expirationTime: NoWork, + baseState, + firstUpdate: null, + lastUpdate: null, + firstRenderPhaseUpdate: null, + lastRenderPhaseUpdate: null, + firstEffect: null, + lastEffect: null, + }; + if (__DEV__) { + queue.isProcessing = false; + } + return queue; +} + +function cloneUpdateQueue(currentQueue) { + const queue = { + expirationTime: currentQueue.expirationTime, + baseState: currentQueue.baseState, + firstUpdate: currentQueue.firstUpdate, + lastUpdate: currentQueue.lastUpdate, + + // These are only valid for the lifetime of a single work-in-progress. + firstRenderPhaseUpdate: null, + lastRenderPhaseUpdate: null, + firstEffect: null, + lastEffect: null, + }; + if (__DEV__) { + queue.isProcessing = false; + } + return queue; +} + +function createUpdate() { + return { + tag: NoOp, + payload: null, + expirationTime: NoWork, + next: null, + nextEffect: null, + }; +} + +export function createStateReplace( + payload: P, + expirationTime: ExpirationTime, +): U { + const update = (createUpdate(): any); + update.tag = ReplaceState; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +export function createStateUpdate( + payload: P, + expirationTime: ExpirationTime, +): U { + const update = (createUpdate(): any); + update.tag = UpdateState; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +export function createForceUpdate(expirationTime): U { + const update = (createUpdate(): any); + update.tag = ForceUpdate; + update.expirationTime = expirationTime; + return update; +} + +export function createDeriveStateFromPropsUpdate(expirationTime) { + const update = (createUpdate(): any); + update.tag = DeriveStateFromPropsUpdate; + update.expirationTime = expirationTime; + return update; +} + +export function createCatchUpdate( + payload: P, + expirationTime: ExpirationTime, +): U { + const update = (createUpdate(): any); + update.tag = CaptureAndLogError; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +export function createCallbackEffect( + payload: P, + expirationTime: ExpirationTime, +): U { + const update = (createUpdate(): any); + update.tag = Callback; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +function appendUpdateToQueue(queue, update, expirationTime) { + // Append the update to the end of the list. + if (queue.lastUpdate === null) { + // Queue is empty + queue.firstUpdate = queue.lastUpdate = update; + } else { + queue.lastUpdate.next = update; + queue.lastUpdate = update; + } + if ( + queue.expirationTime === NoWork || + queue.expirationTime > expirationTime + ) { + // The incoming update has the earliest expiration of any update in the + // queue. Update the queue's expiration time. + queue.expirationTime = expirationTime; + } +} + +export function enqueueUpdate( + owner: UpdateQueueOwner, + update: Update, + expirationTime: ExpirationTime, +) { + // Update queues are created lazily. + const alternate = owner.alternate; + let queue1; + let queue2; + if (alternate === null) { + // There's only one owner. + queue1 = owner.updateQueue; + queue2 = null; + if (queue1 === null) { + queue1 = owner.updateQueue = createUpdateQueue(owner.memoizedState); + } + } else { + // There are two owners. + queue1 = owner.updateQueue; + queue2 = alternate.updateQueue; + if (queue1 === null) { + if (queue2 === null) { + // Neither owner has an update queue. Create new ones. + queue1 = owner.updateQueue = createUpdateQueue(owner.memoizedState); + queue2 = alternate.updateQueue = createUpdateQueue( + alternate.memoizedState, + ); + } else { + // Only one owner has an update queue. Clone to create a new one. + queue1 = owner.updateQueue = cloneUpdateQueue(queue2); + } + } else { + if (queue2 === null) { + // Only one owner has an update queue. Clone to create a new one. + queue2 = alternate.updateQueue = cloneUpdateQueue(queue1); + } else { + // Both owners have an update queue. + } + } + } + if (queue2 === null || queue1 === queue2) { + // There's only a single queue. + appendUpdateToQueue(queue1, update, expirationTime); + } else { + // There are two queues. We need to append the update to both queues, + // while accounting for the persistent structure of the list — we don't + // want the same update to be added multiple times. + if (queue1.lastUpdate === null || queue2.lastUpdate === null) { + // One of the queues is not empty. We must add the update to both queues. + appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue2, update, expirationTime); + } else { + // Both queues are non-empty. The last update is the same in both lists, + // because of structural sharing. So, only append to one of the lists. + appendUpdateToQueue(queue1, update, expirationTime); + // But we still need to update the `lastUpdate` pointer of queue2. + queue2.lastUpdate = update; + } + } + + if (__DEV__) { + if ( + owner.tag === ClassComponent && + (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && + !didWarnUpdateInsideUpdate + ) { + warning( + false, + 'An update (setState, replaceState, or forceUpdate) was scheduled ' + + 'from inside an update function. Update functions should be pure, ' + + 'with zero side-effects. Consider using componentDidUpdate or a ' + + 'callback.', + ); + didWarnUpdateInsideUpdate = true; + } + } +} + +export function enqueueRenderPhaseUpdate( + workInProgressOwner: UpdateQueueOwner, + update: Update, + renderExpirationTime: ExpirationTime, +) { + // Render phase updates go into a separate list, and only on the work-in- + // progress queue. + let workInProgressQueue = workInProgressOwner.updateQueue; + if (workInProgressQueue === null) { + workInProgressQueue = workInProgressOwner.updateQueue = createUpdateQueue( + workInProgressOwner.memoizedState, + ); + } else { + // TODO: I put this here rather than createWorkInProgress so that we don't + // clone the queue unnecessarily. There's probably a better way to + // structure this. + workInProgressQueue = ensureWorkInProgressQueueIsAClone( + workInProgressOwner, + workInProgressQueue, + ); + } + + // Append the update to the end of the list. + if (workInProgressQueue.lastRenderPhaseUpdate === null) { + // This is the first render phase update + workInProgressQueue.firstRenderPhaseUpdate = workInProgressQueue.lastRenderPhaseUpdate = update; + } else { + workInProgressQueue.lastRenderPhaseUpdate.next = update; + workInProgressQueue.lastRenderPhaseUpdate = update; + } + if ( + workInProgressQueue.expirationTime === NoWork || + workInProgressQueue.expirationTime > renderExpirationTime + ) { + // The incoming update has the earliest expiration of any update in the + // queue. Update the queue's expiration time. + workInProgressQueue.expirationTime = renderExpirationTime; + } +} + +function addToEffectList(queue, update) { + // Set this to null, in case it was mutated during an aborted render. + update.nextEffect = null; + if (queue.lastEffect === null) { + queue.firstEffect = queue.lastEffect = update; + } else { + queue.lastEffect.nextEffect = update; + queue.lastEffect = update; + } +} + +function processSingleClassUpdate( + workInProgress: Fiber, + queue: ClassUpdateQueue, + update: ClassUpdate, + prevState: State, +): State { + const payload = update.payload; + switch (update.tag) { + case ReplaceState: { + if (typeof payload === 'function') { + // Updater function + const instance = workInProgress.stateNode; + const nextProps = workInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the updater an extra time to help detect side-effects. + payload.call(instance, prevState, nextProps); + } + + return payload.call(instance, prevState, nextProps); + } + // State object + return payload; + } + case UpdateState: { + let partialState; + if (typeof payload === 'function') { + // Updater function + const instance = workInProgress.stateNode; + const nextProps = workInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the updater an extra time to help detect side-effects. + payload.call(instance, prevState, nextProps); + } + + partialState = payload.call(instance, prevState, nextProps); + } else { + // Partial state object + partialState = payload; + } + if (partialState === null || partialState === undefined) { + // Null and undefined are treated as no-ops. + return prevState; + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } + case ForceUpdate: { + workInProgress.effectTag |= ForceUpdateEffect; + return prevState; + } + case DeriveStateFromPropsUpdate: { + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + const nextProps = workInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromProps(nextProps, prevState); + } + + const partialState = getDerivedStateFromProps(nextProps, prevState); + + if (__DEV__) { + warnOnUndefinedDerivedState(workInProgress, partialState); + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } + case CaptureAndLogError: { + const instance = workInProgress.stateNode; + if (typeof instance.componentDidCatch === 'function') { + workInProgress.effectTag |= UpdateQueueEffect; + addToEffectList(queue, update); + } + } + // Intentional fall-through to the next case, to calculate the derived state + // eslint-disable-next-line no-fallthrough + case CaptureError: { + const errorInfo = update.payload; + const getDerivedStateFromCatch = + workInProgress.type.getDerivedStateFromCatch; + + workInProgress.effectTag = + (workInProgress.effectTag & ~ShouldCapture) | DidCapture; + + if ( + enableGetDerivedStateFromCatch && + typeof getDerivedStateFromCatch === 'function' + ) { + const error = errorInfo.value; + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromCatch(error); + } + + // TODO: Pass prevState as second argument? + const partialState = getDerivedStateFromCatch(error); + + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } else { + return prevState; + } + } + case Callback: { + workInProgress.effectTag |= UpdateQueueEffect; + addToEffectList(queue, update); + return prevState; + } + default: + return prevState; + } +} + +function processSingleRootUpdate( + workInProgress: Fiber, + queue: RootUpdateQueue, + update: RootUpdate, + prevChildren: ReactNodeList, +): ReactNodeList { + switch (update.tag) { + case ReplaceState: { + const nextChildren = update.payload; + return nextChildren; + } + case CaptureAndLogError: { + workInProgress.effectTag = + (workInProgress.effectTag & ~ShouldCapture) | DidCapture; + } + // Intentional fall-through to the next case, to calculate the derived state + // eslint-disable-next-line no-fallthrough + case CaptureError: { + workInProgress.effectTag |= UpdateQueueEffect; + addToEffectList(queue, update); + // Unmount the root by rendering null. + return null; + } + case Callback: { + workInProgress.effectTag |= UpdateQueueEffect; + addToEffectList(queue, update); + return prevChildren; + } + default: + return prevChildren; + } +} + +function processSingleUpdate( + typeOfUpdateQueue, + owner, + queue, + update, + prevState, +) { + switch (typeOfUpdateQueue) { + case ClassUpdateQueue: + const classUpdate: ClassUpdate = (update: any); + return processSingleClassUpdate(owner, queue, classUpdate, prevState); + case RootUpdateQueue: + const rootUpdate: RootUpdate = (update: any); + return processSingleRootUpdate(owner, queue, rootUpdate, prevState); + default: + return prevState; + } +} + +function ensureWorkInProgressQueueIsAClone(owner, queue) { + const alternate = owner.alternate; + if (alternate !== null) { + // If the work-in-progress queue is equal to the current queue, + // we need to clone it first. + if (queue === alternate.updateQueue) { + queue = owner.updateQueue = cloneUpdateQueue(queue); + } + } + return queue; +} + +export function processClassUpdateQueue( + workInProgress: Fiber, + queue: ClassUpdateQueueType, + renderExpirationTime: ExpirationTime, +) { + return processUpdateQueue( + ClassUpdateQueue, + workInProgress, + queue, + renderExpirationTime, + ); +} + +export function processRootUpdateQueue( + workInProgress: Fiber, + queue: RootUpdateQueueType, + renderExpirationTime, +) { + return processUpdateQueue( + RootUpdateQueue, + workInProgress, + queue, + renderExpirationTime, + ); +} + +function processUpdateQueue( + typeOfUpdateQueue, + owner, + queue, + renderExpirationTime, +): void { + if ( + queue.expirationTime === NoWork || + queue.expirationTime > renderExpirationTime + ) { + // Insufficient priority. Bailout. + return; + } + + queue = ensureWorkInProgressQueueIsAClone(owner, queue); + + if (__DEV__) { + queue.isProcessing = true; + } + + // These values may change as we process the queue. + let newBaseState = queue.baseState; + let newFirstUpdate = null; + let newExpirationTime = NoWork; + + // Iterate through the list of updates to compute the result. + let update = queue.firstUpdate; + let resultState = newBaseState; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstUpdate === null) { + // This is the first skipped update. It will be the first update in + // the new list. + newFirstUpdate = update; + // Since this is the first update that was skipped, the current result + // is the new base state. + newBaseState = resultState; + } + // Since this update will remain in the list, update the remaining + // expiration time. + if ( + newExpirationTime === NoWork || + newExpirationTime > updateExpirationTime + ) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. Process it and compute + // a new result. + resultState = processSingleUpdate( + typeOfUpdateQueue, + owner, + queue, + update, + resultState, + ); + } + // Continue to the next update. + update = update.next; + } + + // Separately, iterate though the list of render phase updates. + let newFirstRenderPhaseUpdate = null; + update = queue.firstRenderPhaseUpdate; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstRenderPhaseUpdate === null) { + // This is the first skipped render phase update. It will be the first + // update in the new list. + newFirstUpdate = update; + // If this is the first update that was skipped (including the non- + // render phase updates!), the current result is the new base state. + if (newFirstUpdate === null) { + newBaseState = resultState; + } + } + // Since this update will remain in the list, update the remaining + // expiration time. + if ( + newExpirationTime === NoWork || + newExpirationTime > updateExpirationTime + ) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. Process it and compute + // a new result. + resultState = processSingleUpdate( + typeOfUpdateQueue, + owner, + queue, + update, + resultState, + ); + } + update = update.next; + } + if (newFirstUpdate === null) { + queue.lastUpdate = null; + } + if (newFirstRenderPhaseUpdate === null) { + queue.lastRenderPhaseUpdate = null; + } + if (newFirstUpdate === null && newFirstRenderPhaseUpdate === null) { + // We processed every update, without skipping. That means the new base + // state is the same as the result state. + newBaseState = resultState; + } + + queue.baseState = newBaseState; + queue.firstUpdate = newFirstUpdate; + queue.firstRenderPhaseUpdate = newFirstRenderPhaseUpdate; + queue.expirationTime = newExpirationTime; + + owner.memoizedState = resultState; + + if (__DEV__) { + queue.isProcessing = false; + } +} + +function logError(boundary: Fiber, errorInfo: CapturedValue) { + const source = errorInfo.source; + let stack = errorInfo.stack; + if (stack === null) { + stack = getStackAddendumByWorkInProgressFiber(source); + } + + const capturedError: CapturedError = { + componentName: source !== null ? getComponentName(source) : null, + componentStack: stack !== null ? stack : '', + error: errorInfo.value, + errorBoundary: null, + errorBoundaryName: null, + errorBoundaryFound: false, + willRetry: false, + }; + + if (boundary !== null && boundary.tag === ClassComponent) { + capturedError.errorBoundary = boundary.stateNode; + capturedError.errorBoundaryName = getComponentName(boundary); + capturedError.errorBoundaryFound = true; + capturedError.willRetry = true; + } + + try { + logCapturedError(capturedError); + } catch (e) { + // Prevent cycle if logCapturedError() throws. + // A cycle may still occur if logCapturedError renders a component that throws. + const suppressLogging = e && e.suppressReactErrorLogging; + if (!suppressLogging) { + console.error(e); + } + } +} + +export type UpdateQueueMethods = { + commitClassUpdateQueue( + owner: Fiber, + finishedQueue: ClassUpdateQueueType, + renderExpirationTime: ExpirationTime, + ): void, + commitRootUpdateQueue( + owner: Fiber, + finishedQueue: RootUpdateQueueType, + renderExpirationTime: ExpirationTime, + ): void, +}; + +export default function( + config: HostConfig, + markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, + onUncaughtError, +): UpdateQueueMethods { + const {getPublicInstance} = config; + + function callCallbackEffect(effect, context) { + // Change the effect to no-op so it doesn't fire more than once. + const callback = effect.payload; + + effect.tag = NoOp; + effect.payload = null; + + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(context); + } + + function commitClassEffect( + finishedWork: Fiber, + effect: ClassUpdate, + ) { + switch (effect.tag) { + case Callback: { + const instance = finishedWork.stateNode; + callCallbackEffect(effect, instance); + break; + } + case CaptureAndLogError: { + // Change the tag to CaptureError so that we derive state + // correctly on rebase, but we don't log more than once. + effect.tag = CaptureError; + + const errorInfo = effect.payload; + const instance = finishedWork.stateNode; + const ctor = finishedWork.type; + + if ( + !enableGetDerivedStateFromCatch || + typeof ctor.getDerivedStateFromCatch !== 'function' + ) { + // To preserve the preexisting retry behavior of error boundaries, + // we keep track of which ones already failed during this batch. + // This gets reset before we yield back to the browser. + // TODO: Warn in strict mode if getDerivedStateFromCatch is + // not defined. + markLegacyErrorBoundaryAsFailed(instance); + } + + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + const error = errorInfo.value; + const stack = errorInfo.stack; + logError(finishedWork, errorInfo); + instance.componentDidCatch(error, { + componentStack: stack !== null ? stack : '', + }); + break; + } + } + } + + function commitRootEffect(finishedWork: Fiber, effect: RootUpdate) { + switch (effect.tag) { + case Callback: { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } + } + callCallbackEffect(effect, instance); + break; + } + case CaptureAndLogError: { + // Change the tag to CaptureError so that we derive state + // correctly on rebase, but we don't log more than once. + effect.tag = CaptureError; + const errorInfo = effect.payload; + const error = errorInfo.value; + + onUncaughtError(error); + + logError(finishedWork, errorInfo); + break; + } + } + } + + function commitEffect(typeOfUpdateQueue, owner, queue, effect) { + switch (typeOfUpdateQueue) { + case ClassUpdateQueue: { + const classEffect: ClassEffect = (effect: any); + commitClassEffect(owner, classEffect); + break; + } + case RootUpdateQueue: { + const rootEffect: ClassEffect = (effect: any); + commitRootEffect(owner, rootEffect); + break; + } + } + } + + function commitClassUpdateQueue( + owner: Fiber, + finishedQueue: ClassUpdateQueueType, + renderExpirationTime: ExpirationTime, + ): void { + return commitUpdateQueue( + ClassUpdateQueue, + owner, + finishedQueue, + renderExpirationTime, + ); + } + + function commitRootUpdateQueue( + owner: Fiber, + finishedQueue: RootUpdateQueueType, + renderExpirationTime: ExpirationTime, + ): void { + return commitUpdateQueue( + RootUpdateQueue, + owner, + finishedQueue, + renderExpirationTime, + ); + } + + function commitUpdateQueue( + typeOfUpdateQueue, + owner, + finishedQueue, + renderExpirationTime, + ): void { + // If the finished render included render phase updates, and there are still + // lower priority updates left over, we need to keep the render phase updates + // in the queue so that they are rebased and not dropped once we process the + // queue again at the lower priority. + if (finishedQueue.firstRenderPhaseUpdate !== null) { + // Join the render phase update list to the end of the normal list. + if (finishedQueue.lastUpdate === null) { + // This should be unreachable. + if (__DEV__) { + warning(false, 'Expected a non-empty queue.'); + } + } else { + finishedQueue.lastUpdate.next = finishedQueue.firstRenderPhaseUpdate; + finishedQueue.lastUpdate = finishedQueue.lastRenderPhaseUpdate; + } + if ( + finishedQueue.expirationTime === NoWork || + finishedQueue.expirationTime > renderExpirationTime + ) { + // Update the queue's expiration time. + finishedQueue.expirationTime = renderExpirationTime; + } + // Clear the list of render phase updates. + finishedQueue.firstRenderPhaseUpdate = finishedQueue.lastRenderPhaseUpdate = null; + } + + // Commit the effects + let effect = finishedQueue.firstEffect; + finishedQueue.firstEffect = finishedQueue.lastEffect = null; + while (effect !== null) { + commitEffect(typeOfUpdateQueue, owner, finishedQueue, effect); + effect = effect.nextEffect; + } + } + + return { + commitClassUpdateQueue, + commitRootUpdateQueue, + }; +} diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index 91de8d285da29..e9f22dec8c16f 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1003,6 +1003,7 @@ describe('ReactIncremental', () => { instance.setState(updater); ReactNoop.flush(); expect(instance.state.num).toEqual(2); + instance.setState(updater); ReactNoop.render(); ReactNoop.flush(); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js index 628e1c38806b5..731865da65d7c 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js @@ -427,6 +427,8 @@ describe('ReactIncrementalTriangle', () => { function simulate(...actions) { const gen = simulateAndYield(); + // Call this once to prepare the generator + gen.next(); // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (let action of actions) { gen.next(action); diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index 40e37fe439489..ffe5706d1c022 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -298,7 +298,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update ⚛ (Committing Snapshot Effects: 0 Total) ⚛ (Committing Host Effects: 2 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) ⚛ (React Tree Reconciliation: Completed Root) ⚛ Boundary [update] @@ -324,7 +324,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` ⚛ (Committing Changes) ⚛ (Committing Snapshot Effects: 0 Total) ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 0 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) ⚛ (Waiting for async callback... will force flush in 5230 ms) @@ -406,8 +406,8 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = ⚛ (Committing Changes) ⚛ (Committing Snapshot Effects: 0 Total) - ⚛ (Committing Host Effects: 1 Total) - ⚛ (Calling Lifecycle Methods: 1 Total) + ⚛ (Committing Host Effects: 0 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) " `; diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 82e8c3342fc70..19699f5749096 100644 --- a/packages/shared/ReactTypeOfSideEffect.js +++ b/packages/shared/ReactTypeOfSideEffect.js @@ -19,14 +19,14 @@ export const Update = /* */ 0b000000000100; export const PlacementAndUpdate = /* */ 0b000000000110; export const Deletion = /* */ 0b000000001000; export const ContentReset = /* */ 0b000000010000; -export const Callback = /* */ 0b000000100000; +export const UpdateQueue = /* */ 0b000000100000; export const DidCapture = /* */ 0b000001000000; export const Ref = /* */ 0b000010000000; -export const ErrLog = /* */ 0b000100000000; -export const Snapshot = /* */ 0b100000000000; +export const Snapshot = /* */ 0b000100000000; +export const ForceUpdate = /* */ 0b001000000000; // Union of all host effects -export const HostEffectMask = /* */ 0b100111111111; +export const HostEffectMask = /* */ 0b001111111111; -export const Incomplete = /* */ 0b001000000000; -export const ShouldCapture = /* */ 0b010000000000; +export const Incomplete = /* */ 0b010000000000; +export const ShouldCapture = /* */ 0b100000000000;