From 2bc15bfd08c349b53c6698e02306d9ddf3d6867e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 9 Apr 2018 17:46:37 -0700 Subject: [PATCH 01/10] 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 | 59 +- .../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, 1315 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..0e299388b893e 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); @@ -681,7 +681,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 +808,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 +983,12 @@ export default function( onUncaughtError(thrownValue); break; } - throwException(returnFiber, sourceFiber, thrownValue); + throwException( + returnFiber, + sourceFiber, + thrownValue, + nextRenderExpirationTime, + ); nextUnitOfWork = completeUnitOfWork(sourceFiber); } break; @@ -1023,18 +1037,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; From de45514cd6831c91f790182af862311db2249bb4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 19 Apr 2018 17:10:46 -0700 Subject: [PATCH 02/10] Remove first class UpdateQueue types and use closures instead I tried to avoid this at first, since we avoid it everywhere else in the Fiber codebase, but since updates are not in a hot path, the trade off with file size seems worth it. --- packages/react-noop-renderer/src/ReactNoop.js | 14 +- .../src/ReactFiberBeginWork.js | 26 +- .../src/ReactFiberClassComponent.js | 218 +++-- .../src/ReactFiberCommitWork.js | 55 +- .../src/ReactFiberReconciler.js | 48 +- .../src/ReactFiberScheduler.js | 46 +- .../src/ReactFiberUnwindWork.js | 110 ++- .../react-reconciler/src/ReactUpdateQueue.js | 866 +++++------------- 8 files changed, 603 insertions(+), 780 deletions(-) diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 2ee461d94895e..5cd6df0fbd29b 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -526,23 +526,15 @@ const ReactNoop = { function logUpdateQueue(updateQueue: UpdateQueue, depth) { log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); - const firstUpdate = updateQueue.first; + const firstUpdate = updateQueue.firstUpdate; if (!firstUpdate) { return; } - log( - ' '.repeat(depth + 1) + '~', - firstUpdate && firstUpdate.partialState, - firstUpdate.callback ? 'with callback' : '', - '[' + firstUpdate.expirationTime + ']', - ); - let next; - while ((next = firstUpdate.next)) { + log(' '.repeat(depth + 1) + '~', '[' + firstUpdate.expirationTime + ']'); + while (firstUpdate.next) { log( ' '.repeat(depth + 1) + '~', - next.partialState, - next.callback ? 'with callback' : '', '[' + firstUpdate.expirationTime + ']', ); } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 47d6b2be31e17..27e01836c0525 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -54,18 +54,15 @@ import warning from 'fbjs/lib/warning'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {cancelWorkTimer} from './ReactDebugFiberPerf'; -import ReactFiberClassComponent from './ReactFiberClassComponent'; +import ReactFiberClassComponent, { + createGetDerivedStateFromPropsUpdate, +} from './ReactFiberClassComponent'; import { mountChildFibers, reconcileChildFibers, cloneChildFibers, } from './ReactChildFiber'; -import { - createDeriveStateFromPropsUpdate, - enqueueRenderPhaseUpdate, - processClassUpdateQueue, - processRootUpdateQueue, -} from './ReactUpdateQueue'; +import {enqueueRenderPhaseUpdate, processUpdateQueue} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -415,7 +412,7 @@ export default function( let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { const prevChildren = workInProgress.memoizedState; - processRootUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); const nextChildren = workInProgress.memoizedState; if (nextChildren === prevChildren) { @@ -595,15 +592,14 @@ export default function( const getDerivedStateFromProps = Component.getDerivedStateFromProps; if (typeof getDerivedStateFromProps === 'function') { - const update = createDeriveStateFromPropsUpdate(renderExpirationTime); + const update = createGetDerivedStateFromPropsUpdate( + getDerivedStateFromProps, + renderExpirationTime, + ); enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processClassUpdateQueue( - workInProgress, - updateQueue, - renderExpirationTime, - ); + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); } } @@ -1080,7 +1076,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 processClassUpdateQueue. + // is handled by processUpdateQueue. } function beginWork( diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 75f63abf0da33..dff6eac796973 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -31,12 +31,8 @@ import {StrictMode} from './ReactTypeOfMode'; import { enqueueUpdate, enqueueRenderPhaseUpdate, - processClassUpdateQueue, - createStateUpdate, - createStateReplace, - createCallbackEffect, - createDeriveStateFromPropsUpdate, - createForceUpdate, + processUpdateQueue, + createUpdate, } from './ReactUpdateQueue'; const fakeInternalInstance = {}; @@ -46,6 +42,8 @@ let didWarnAboutStateAssignmentForComponent; let didWarnAboutUninitializedState; let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; let didWarnAboutLegacyLifecyclesAndDerivedState; +let didWarnAboutUndefinedDerivedState; +let warnOnUndefinedDerivedState; let warnOnInvalidCallback; if (__DEV__) { @@ -53,6 +51,7 @@ if (__DEV__) { didWarnAboutUninitializedState = new Set(); didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); + didWarnAboutUndefinedDerivedState = new Set(); const didWarnOnInvalidCallback = new Set(); @@ -73,6 +72,21 @@ if (__DEV__) { } }; + 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, + ); + } + } + }; + // This is so gross but it's at least non-critical and can be removed if // it causes problems. This is meant to give a nicer error message for // ReactDOM15.unstable_renderSubtreeIntoContainer(reactDOM16Component, @@ -95,15 +109,32 @@ if (__DEV__) { Object.freeze(fakeInternalInstance); } -function enqueueDerivedStateFromProps( - workInProgress: Fiber, +export function createGetDerivedStateFromPropsUpdate( + getDerivedStateFromProps: (props: any, state: any) => any, renderExpirationTime: ExpirationTime, -): void { - const getDerivedStateFromProps = workInProgress.type.getDerivedStateFromProps; - if (typeof getDerivedStateFromProps === 'function') { - const update = createDeriveStateFromPropsUpdate(renderExpirationTime); - enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); - } +) { + const update = createUpdate(renderExpirationTime); + update.process = (nextWorkInProgress, prevState) => { + const nextProps = nextWorkInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + nextWorkInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromProps(nextProps, prevState); + } + + const partialState = getDerivedStateFromProps(nextProps, prevState); + + if (__DEV__) { + warnOnUndefinedDerivedState(nextWorkInProgress, partialState); + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + }; + return update; } export default function( @@ -121,57 +152,125 @@ export default function( hasContextChanged, } = legacyContext; + function callCallback(callback, context) { + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(context); + } + const classComponentUpdater = { isMounted, - enqueueSetState(instance, payload, callback) { - const fiber = ReactInstanceMap.get(instance); + enqueueSetState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = createStateUpdate(payload, expirationTime); - enqueueUpdate(fiber, update, expirationTime); + const update = createUpdate(expirationTime); + update.process = (workInProgress, prevState) => { + 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); + }; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - const callbackUpdate = createCallbackEffect(callback, expirationTime); - enqueueUpdate(fiber, callbackUpdate, expirationTime); + update.commit = finishedWork => { + const instance = finishedWork.stateNode; + callCallback(callback, instance); + }; } + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, - enqueueReplaceState(instance, payload, callback) { - const fiber = ReactInstanceMap.get(instance); + enqueueReplaceState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = createStateReplace(payload, expirationTime); - enqueueUpdate(fiber, update, expirationTime); + const update = createUpdate(expirationTime); + update.process = (workInProgress, prevState) => { + 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; + }; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - const callbackUpdate = createCallbackEffect(callback, expirationTime); - enqueueUpdate(fiber, callbackUpdate, expirationTime); + update.commit = finishedWork => { + const instance = finishedWork.stateNode; + callCallback(callback, instance); + }; } + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, - enqueueForceUpdate(instance, callback) { - const fiber = ReactInstanceMap.get(instance); + enqueueForceUpdate(inst, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = createForceUpdate(expirationTime); - enqueueUpdate(fiber, update, expirationTime); + const update = createUpdate(expirationTime); + update.process = (workInProgress, prevState) => { + workInProgress.effectTag |= ForceUpdate; + return prevState; + }; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - const callbackUpdate = createCallbackEffect(callback, expirationTime); - enqueueUpdate(fiber, callbackUpdate, expirationTime); + update.commit = finishedWork => { + const instance = finishedWork.stateNode; + callCallback(callback, instance); + }; } + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, }; @@ -640,14 +739,19 @@ export default function( } } - enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - processClassUpdateQueue( - workInProgress, - updateQueue, + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createGetDerivedStateFromPropsUpdate( + getDerivedStateFromProps, renderExpirationTime, ); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); + } + + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); instance.state = workInProgress.memoizedState; } @@ -664,11 +768,7 @@ export default function( // process them now. updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processClassUpdateQueue( - workInProgress, - updateQueue, - renderExpirationTime, - ); + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); instance.state = workInProgress.memoizedState; } } @@ -720,18 +820,22 @@ export default function( // Only call getDerivedStateFromProps if the props have changed if (oldProps !== newProps) { - enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createGetDerivedStateFromPropsUpdate( + getDerivedStateFromProps, + renderExpirationTime, + ); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); + } } const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processClassUpdateQueue( - workInProgress, - updateQueue, - renderExpirationTime, - ); + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); newState = workInProgress.memoizedState; } @@ -844,18 +948,22 @@ export default function( // Only call getDerivedStateFromProps if the props have changed if (oldProps !== newProps) { - enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createGetDerivedStateFromPropsUpdate( + getDerivedStateFromProps, + renderExpirationTime, + ); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); + } } const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processClassUpdateQueue( - workInProgress, - updateQueue, - renderExpirationTime, - ); + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); newState = workInProgress.memoizedState; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 75e1f3691de7b..e2195e01c4d1d 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 {UpdateQueueMethods} from './ReactUpdateQueue'; +import type {CapturedValue, CapturedError} from './ReactCapturedValue'; import { enableMutatingReconciler, @@ -33,6 +33,7 @@ import { ContentReset, Snapshot, } from 'shared/ReactTypeOfSideEffect'; +import {commitUpdateQueue} from './ReactUpdateQueue'; import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; @@ -40,6 +41,7 @@ import {onCommitUnmount} from './ReactFiberDevToolsHook'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import getComponentName from 'shared/getComponentName'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; +import {logCapturedError} from './ReactFiberErrorLogger'; const { invokeGuardedCallback, @@ -52,9 +54,44 @@ if (__DEV__) { didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); } +export 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, @@ -70,8 +107,6 @@ export default function( ) { const {getPublicInstance, mutation, persistence} = config; - const {commitClassUpdateQueue, commitRootUpdateQueue} = updateQueueMethods; - const callComponentWillUnmountWithTimer = function(current, instance) { startPhaseTimer(current, 'componentWillUnmount'); instance.props = current.memoizedProps; @@ -216,22 +251,14 @@ export default function( } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitClassUpdateQueue( - finishedWork, - updateQueue, - committedExpirationTime, - ); + commitUpdateQueue(finishedWork, updateQueue, committedExpirationTime); } return; } case HostRoot: { const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitRootUpdateQueue( - finishedWork, - updateQueue, - committedExpirationTime, - ); + commitUpdateQueue(finishedWork, updateQueue, committedExpirationTime); } return; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index b9aa3a5c971b8..f71773162150c 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -17,7 +17,7 @@ import { findCurrentHostFiberWithNoPortals, } from 'react-reconciler/reflection'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; -import {HostComponent} from 'shared/ReactTypeOfWork'; +import {ClassComponent, HostComponent} from 'shared/ReactTypeOfWork'; import emptyObject from 'fbjs/lib/emptyObject'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; @@ -26,11 +26,7 @@ import warning from 'fbjs/lib/warning'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import ReactFiberScheduler from './ReactFiberScheduler'; -import { - createStateReplace, - createCallbackEffect, - enqueueUpdate, -} from './ReactUpdateQueue'; +import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -343,22 +339,40 @@ export default function( } } - const update = createStateReplace(element, expirationTime); - enqueueUpdate(current, update, expirationTime); + const update = createUpdate(expirationTime); + + update.process = () => element; callback = callback === undefined ? null : 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.', + if (callback !== null) { + warning( + typeof callback === 'function', + 'render(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callback, + ); + update.commit = finishedWork => { + 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; + } + } + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', callback, ); - } - const callbackUpdate = createCallbackEffect(callback, expirationTime); - enqueueUpdate(current, callbackUpdate, expirationTime); + callback.call(instance); + }; } + enqueueUpdate(current, update, expirationTime); scheduleWork(current, expirationTime); return expirationTime; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 0e299388b893e..646c47c75b00c 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -88,10 +88,7 @@ import { import {AsyncMode} from './ReactTypeOfMode'; import ReactFiberLegacyContext from './ReactFiberContext'; import ReactFiberNewContext from './ReactFiberNewContext'; -import ReactUpdateQueue, { - createCatchUpdate, - enqueueUpdate, -} from './ReactUpdateQueue'; +import {enqueueUpdate} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; @@ -194,16 +191,15 @@ export default function( throwException, unwindWork, unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, } = ReactFiberUnwindWork( hostContext, legacyContext, newContext, scheduleWork, - isAlreadyFailedLegacyErrorBoundary, - ); - const updateQueueMethods = ReactUpdateQueue( - config, markLegacyErrorBoundaryAsFailed, + isAlreadyFailedLegacyErrorBoundary, onUncaughtError, ); const { @@ -217,7 +213,6 @@ export default function( commitDetachRef, } = ReactFiberCommitWork( config, - updateQueueMethods, onCommitPhaseError, scheduleWork, computeExpirationForFiber, @@ -1036,13 +1031,6 @@ export default function( } } - function scheduleCapture(sourceFiber, boundaryFiber, value, expirationTime) { - const capturedValue = createCapturedValue(value, sourceFiber); - const update = createCatchUpdate(capturedValue, expirationTime); - enqueueUpdate(boundaryFiber, update, expirationTime); - scheduleWork(boundaryFiber, expirationTime); - } - function dispatch( sourceFiber: Fiber, value: mixed, @@ -1053,8 +1041,6 @@ export default function( 'dispatch: Cannot dispatch during the render phase.', ); - // TODO: Handle arrays - let fiber = sourceFiber.return; while (fiber !== null) { switch (fiber.tag) { @@ -1066,14 +1052,24 @@ export default function( (typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance)) ) { - scheduleCapture(sourceFiber, fiber, value, expirationTime); + const errorInfo = createCapturedValue(value, sourceFiber); + const update = createClassErrorUpdate( + fiber, + errorInfo, + expirationTime, + ); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); return; } break; - // TODO: Handle async boundaries - case HostRoot: - scheduleCapture(sourceFiber, fiber, value, expirationTime); + case HostRoot: { + const errorInfo = createCapturedValue(value, sourceFiber); + const update = createRootErrorUpdate(errorInfo, expirationTime); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); return; + } } fiber = fiber.return; } @@ -1081,7 +1077,11 @@ export default function( if (sourceFiber.tag === HostRoot) { // Error was thrown at the root. There is no parent, so the root // itself should capture it. - scheduleCapture(sourceFiber, sourceFiber, value, expirationTime); + const rootFiber = sourceFiber; + const errorInfo = createCapturedValue(value, rootFiber); + const update = createRootErrorUpdate(errorInfo, expirationTime); + enqueueUpdate(rootFiber, update, expirationTime); + scheduleWork(rootFiber, expirationTime); } } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 4680f158e4e52..18bc5be578e17 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -12,9 +12,12 @@ import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {HostContext} from './ReactFiberHostContext'; import type {LegacyContext} from './ReactFiberContext'; import type {NewContext} from './ReactFiberNewContext'; +import type {CapturedValue} from './ReactCapturedValue'; +import type {Update} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {enqueueRenderPhaseUpdate, createCatchUpdate} from './ReactUpdateQueue'; +import {enqueueRenderPhaseUpdate, createUpdate} from './ReactUpdateQueue'; +import {logError} from './ReactFiberCommitWork'; import { ClassComponent, @@ -29,8 +32,13 @@ import { Incomplete, ShouldCapture, } from 'shared/ReactTypeOfSideEffect'; +import {StrictMode} from './ReactTypeOfMode'; -import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags'; +import { + enableGetDerivedStateFromCatch, + debugRenderPhaseSideEffects, + debugRenderPhaseSideEffectsForStrictMode, +} from 'shared/ReactFeatureFlags'; export default function( hostContext: HostContext, @@ -41,7 +49,9 @@ export default function( startTime: ExpirationTime, expirationTime: ExpirationTime, ) => void, + markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean, + onUncaughtError: (error: mixed) => void, ) { const {popHostContainer, popHostContext} = hostContext; const { @@ -50,6 +60,91 @@ export default function( } = legacyContext; const {popProvider} = newContext; + function createRootErrorUpdate( + errorInfo: CapturedValue, + expirationTime: ExpirationTime, + ): Update { + const update = createUpdate(expirationTime); + update.process = nextWorkInProgress => { + // Unmount the root by rendering null. + return null; + }; + update.commit = finishedWork => { + const error = errorInfo.value; + onUncaughtError(error); + logError(finishedWork, errorInfo); + }; + return update; + } + + function createClassErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, + ): Update { + const update = createUpdate(expirationTime); + update.process = (workInProgress, prevState) => { + 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; + } + }; + + const inst = fiber.stateNode; + if (inst !== null && typeof inst.componentDidCatch === 'function') { + update.commit = finishedWork => { + 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 : '', + }); + }; + } + return update; + } + function throwException( returnFiber: Fiber, sourceFiber: Fiber, @@ -67,10 +162,9 @@ export default function( do { switch (workInProgress.tag) { case HostRoot: { - // Uncaught error const errorInfo = value; workInProgress.effectTag |= ShouldCapture; - const update = createCatchUpdate(errorInfo, renderExpirationTime); + const update = createRootErrorUpdate(errorInfo, renderExpirationTime); enqueueRenderPhaseUpdate( workInProgress, update, @@ -94,7 +188,11 @@ export default function( workInProgress.effectTag |= ShouldCapture; // Schedule the error boundary to re-render using updated state - const update = createCatchUpdate(errorInfo, renderExpirationTime); + const update = createClassErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); enqueueRenderPhaseUpdate( workInProgress, update, @@ -175,5 +273,7 @@ export default function( throwException, unwindWork, unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, }; } diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 15a57f760ba19..65c34eea4a8ad 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -3,6 +3,8 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @flow */ // UpdateQueue is a linked list of prioritized updates. @@ -102,142 +104,60 @@ // - getDerivedStateFromCatch // - [future] loading state +import type {Fiber} from './ReactFiber'; 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 {UpdateQueue as UpdateQueueEffect} from 'shared/ReactTypeOfSideEffect'; +import {ClassComponent} from 'shared/ReactTypeOfWork'; -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, +export type Update = { 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>); + process: ((workInProgress: Fiber, prevState: State) => State) | null, + commit: ((finishedWork: Fiber) => mixed) | null, -type RootUpdate = - | ({tag: 0} & UpdateShared) - | ({tag: 1} & UpdateShared) - | ({tag: 5} & UpdateShared, RootUpdate>) - | ({tag: 6} & UpdateShared, RootUpdate>) - | ({tag: 7} & UpdateShared<() => mixed, RootUpdate>); + next: Update | null, + nextEffect: Update | null, +}; -type UpdateQueueShared = { +export type UpdateQueue = { expirationTime: ExpirationTime, - baseState: S, - - firstUpdate: U | null, - lastUpdate: U | null, - - firstRenderPhaseUpdate: U | null, - lastRenderPhaseUpdate: U | null, + baseState: State, - firstEffect: U | null, - lastEffect: U | null, + firstUpdate: Update | null, + lastUpdate: Update | null, - // DEV_only - isProcessing?: boolean, -}; + firstRenderPhaseUpdate: Update | null, + lastRenderPhaseUpdate: Update | null, -type ClassUpdateQueueType = UpdateQueueShared< - ClassUpdate, - State, ->; + firstCapturedUpdate: Update | null, + lastCapturedUpdate: Update | null, -type RootUpdateQueueType = UpdateQueueShared; + firstEffect: Update | null, + lastEffect: Update | null, -type UpdateQueueOwner = { - alternate: UpdateQueueOwner | null, - memoizedState: State, + // DEV-only + isProcessing?: boolean, }; -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 = { +function createUpdateQueue(baseState: State): UpdateQueue { + const queue: UpdateQueue = { expirationTime: NoWork, baseState, firstUpdate: null, lastUpdate: null, firstRenderPhaseUpdate: null, lastRenderPhaseUpdate: null, + firstCapturedUpdate: null, + lastCapturedUpdate: null, firstEffect: null, lastEffect: null, }; @@ -247,8 +167,10 @@ function createUpdateQueue(baseState) { return queue; } -function cloneUpdateQueue(currentQueue) { - const queue = { +function cloneUpdateQueue( + currentQueue: UpdateQueue, +): UpdateQueue { + const queue: UpdateQueue = { expirationTime: currentQueue.expirationTime, baseState: currentQueue.baseState, firstUpdate: currentQueue.firstUpdate, @@ -257,6 +179,12 @@ function cloneUpdateQueue(currentQueue) { // These are only valid for the lifetime of a single work-in-progress. firstRenderPhaseUpdate: null, lastRenderPhaseUpdate: null, + + // TODO: With resuming, if we bail out and resuse the child tree, we should + // keep these effects. + firstCapturedUpdate: null, + lastCapturedUpdate: null, + firstEffect: null, lastEffect: null, }; @@ -266,75 +194,23 @@ function cloneUpdateQueue(currentQueue) { return queue; } -function createUpdate() { +export function createUpdate(expirationTime: ExpirationTime): Update<*> { return { - tag: NoOp, - payload: null, - expirationTime: NoWork, + expirationTime: expirationTime, + + process: null, + commit: null, + next: null, nextEffect: null, }; } -export function createStateReplace( - payload: P, +function appendUpdateToQueue( + queue: UpdateQueue, + update: Update, 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 @@ -353,40 +229,40 @@ function appendUpdateToQueue(queue, update, expirationTime) { } } -export function enqueueUpdate( - owner: UpdateQueueOwner, - update: Update, +export function enqueueUpdate( + fiber: Fiber, + update: Update, expirationTime: ExpirationTime, ) { // Update queues are created lazily. - const alternate = owner.alternate; + const alternate = fiber.alternate; let queue1; let queue2; if (alternate === null) { - // There's only one owner. - queue1 = owner.updateQueue; + // There's only one fiber. + queue1 = fiber.updateQueue; queue2 = null; if (queue1 === null) { - queue1 = owner.updateQueue = createUpdateQueue(owner.memoizedState); + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); } } else { // There are two owners. - queue1 = owner.updateQueue; + queue1 = fiber.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); + // Neither fiber has an update queue. Create new ones. + queue1 = fiber.updateQueue = createUpdateQueue(fiber.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); + // Only one fiber has an update queue. Clone to create a new one. + queue1 = fiber.updateQueue = cloneUpdateQueue(queue2); } } else { if (queue2 === null) { - // Only one owner has an update queue. Clone to create a new one. + // Only one fiber has an update queue. Clone to create a new one. queue2 = alternate.updateQueue = cloneUpdateQueue(queue1); } else { // Both owners have an update queue. @@ -415,7 +291,7 @@ export function enqueueUpdate( if (__DEV__) { if ( - owner.tag === ClassComponent && + fiber.tag === ClassComponent && (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && !didWarnUpdateInsideUpdate ) { @@ -431,24 +307,24 @@ export function enqueueUpdate( } } -export function enqueueRenderPhaseUpdate( - workInProgressOwner: UpdateQueueOwner, - update: Update, +export function enqueueRenderPhaseUpdate( + workInProgress: Fiber, + update: Update, renderExpirationTime: ExpirationTime, ) { // Render phase updates go into a separate list, and only on the work-in- // progress queue. - let workInProgressQueue = workInProgressOwner.updateQueue; + let workInProgressQueue = workInProgress.updateQueue; if (workInProgressQueue === null) { - workInProgressQueue = workInProgressOwner.updateQueue = createUpdateQueue( - workInProgressOwner.memoizedState, + workInProgressQueue = workInProgress.updateQueue = createUpdateQueue( + workInProgress.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, + workInProgress, workInProgressQueue, ); } @@ -471,7 +347,50 @@ export function enqueueRenderPhaseUpdate( } } -function addToEffectList(queue, update) { +export function enqueueCapturedUpdate( + workInProgress: Fiber, + update: Update, + renderExpirationTime: ExpirationTime, +) { + // Captured updates go into a separate list, and only on the work-in- + // progress queue. + let workInProgressQueue = workInProgress.updateQueue; + if (workInProgressQueue === null) { + workInProgressQueue = workInProgress.updateQueue = createUpdateQueue( + workInProgress.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( + workInProgress, + workInProgressQueue, + ); + } + + // Append the update to the end of the list. + if (workInProgressQueue.lastCapturedUpdate === null) { + // This is the first render phase update + workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update; + } else { + workInProgressQueue.lastCapturedUpdate.next = update; + workInProgressQueue.lastCapturedUpdate = 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: UpdateQueue, + update: Update, +) { // Set this to null, in case it was mutated during an aborted render. update.nextEffect = null; if (queue.lastEffect === null) { @@ -482,233 +401,44 @@ function addToEffectList(queue, update) { } } -function processSingleClassUpdate( +function processSingleUpdate( workInProgress: Fiber, - queue: ClassUpdateQueue, - update: ClassUpdate, + queue: UpdateQueue, + update: Update, 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; + const commit = update.commit; + const process = update.process; + if (commit !== null) { + workInProgress.effectTag |= UpdateQueueEffect; + addToEffectList(queue, update); + } + if (process !== null) { + return process(workInProgress, prevState); + } else { + return prevState; } } -function processSingleRootUpdate( +function ensureWorkInProgressQueueIsAClone( 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) { + queue: UpdateQueue, +): UpdateQueue { + const current = workInProgress.alternate; + if (current !== 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); + if (queue === current.updateQueue) { + queue = workInProgress.updateQueue = cloneUpdateQueue(queue); } } return queue; } -export function processClassUpdateQueue( +export function processUpdateQueue( workInProgress: Fiber, - queue: ClassUpdateQueueType, + queue: UpdateQueue, 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 || @@ -718,7 +448,7 @@ function processUpdateQueue( return; } - queue = ensureWorkInProgressQueueIsAClone(owner, queue); + queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); if (__DEV__) { queue.isProcessing = true; @@ -756,8 +486,7 @@ function processUpdateQueue( // This update does have sufficient priority. Process it and compute // a new result. resultState = processSingleUpdate( - typeOfUpdateQueue, - owner, + workInProgress, queue, update, resultState, @@ -777,7 +506,7 @@ function processUpdateQueue( if (newFirstRenderPhaseUpdate === null) { // This is the first skipped render phase update. It will be the first // update in the new list. - newFirstUpdate = update; + newFirstRenderPhaseUpdate = 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) { @@ -796,8 +525,7 @@ function processUpdateQueue( // This update does have sufficient priority. Process it and compute // a new result. resultState = processSingleUpdate( - typeOfUpdateQueue, - owner, + workInProgress, queue, update, resultState, @@ -805,13 +533,63 @@ function processUpdateQueue( } update = update.next; } + + // Separately, iterate though the list of captured updates. + let newFirstCapturedUpdate = null; + update = queue.firstCapturedUpdate; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstCapturedUpdate === null) { + // This is the first skipped captured update. It will be the first + // update in the new list. + newFirstCapturedUpdate = update; + // If this is the first update that was skipped, the current result is + // the new base state. + if (newFirstUpdate === null && newFirstRenderPhaseUpdate === 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( + workInProgress, + queue, + update, + resultState, + ); + } + update = update.next; + } + if (newFirstUpdate === null) { queue.lastUpdate = null; } if (newFirstRenderPhaseUpdate === null) { queue.lastRenderPhaseUpdate = null; + } else { + workInProgress.effectTag |= UpdateQueueEffect; + } + if (newFirstCapturedUpdate === null) { + queue.lastCapturedUpdate = null; + } else { + workInProgress.effectTag |= UpdateQueueEffect; } - if (newFirstUpdate === null && newFirstRenderPhaseUpdate === null) { + if ( + newFirstUpdate === null && + newFirstRenderPhaseUpdate === null && + newFirstCapturedUpdate === null + ) { // We processed every update, without skipping. That means the new base // state is the same as the result state. newBaseState = resultState; @@ -820,247 +598,55 @@ function processUpdateQueue( queue.baseState = newBaseState; queue.firstUpdate = newFirstUpdate; queue.firstRenderPhaseUpdate = newFirstRenderPhaseUpdate; + queue.firstCapturedUpdate = newFirstCapturedUpdate; queue.expirationTime = newExpirationTime; - owner.memoizedState = resultState; + workInProgress.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; - } +export function commitUpdateQueue( + finishedWork: Fiber, + finishedQueue: UpdateQueue, + renderExpirationTime: ExpirationTime, +): 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) { + finishedQueue.lastUpdate.next = finishedQueue.firstRenderPhaseUpdate; + finishedQueue.lastUpdate = finishedQueue.lastRenderPhaseUpdate; } + // Clear the list of render phase updates. + finishedQueue.firstRenderPhaseUpdate = finishedQueue.lastRenderPhaseUpdate = null; } - 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; + // Same with captured updates + if (finishedQueue.firstCapturedUpdate !== null) { + // Join the render phase update list to the end of the normal list. + if (finishedQueue.lastUpdate !== null) { + finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate; + finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate; } - - // Commit the effects - let effect = finishedQueue.firstEffect; - finishedQueue.firstEffect = finishedQueue.lastEffect = null; - while (effect !== null) { - commitEffect(typeOfUpdateQueue, owner, finishedQueue, effect); - effect = effect.nextEffect; + // Clear the list of render phase updates. + finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null; + } + + // Commit the effects + let effect = finishedQueue.firstEffect; + finishedQueue.firstEffect = finishedQueue.lastEffect = null; + while (effect !== null) { + const commit = effect.commit; + if (commit !== null) { + effect.commit = null; + commit(finishedWork); } + effect = effect.nextEffect; } - - return { - commitClassUpdateQueue, - commitRootUpdateQueue, - }; } From 23e859c8f4473930339da9b67030f484f0c751d6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 19 Apr 2018 17:14:07 -0700 Subject: [PATCH 03/10] Store captured errors on a separate part of the update queue This way they can be reused independently of updates like getDerivedStateFromProps. This will be important for resuming. --- .../react-reconciler/src/ReactFiberUnwindWork.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 18bc5be578e17..1923dd7f1cdb6 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -16,7 +16,7 @@ import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {enqueueRenderPhaseUpdate, createUpdate} from './ReactUpdateQueue'; +import {enqueueCapturedUpdate, createUpdate} from './ReactUpdateQueue'; import {logError} from './ReactFiberCommitWork'; import { @@ -165,11 +165,7 @@ export default function( const errorInfo = value; workInProgress.effectTag |= ShouldCapture; const update = createRootErrorUpdate(errorInfo, renderExpirationTime); - enqueueRenderPhaseUpdate( - workInProgress, - update, - renderExpirationTime, - ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } case ClassComponent: @@ -193,11 +189,7 @@ export default function( errorInfo, renderExpirationTime, ); - enqueueRenderPhaseUpdate( - workInProgress, - update, - renderExpirationTime, - ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } break; From 65f5f30dfd22ad45798764984f8e612ded6270c4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 19 Apr 2018 17:25:27 -0700 Subject: [PATCH 04/10] Revert back to storing hasForceUpdate on the update queue Instead of using the effect tag. Ideally, this would be part of the return type of processUpdateQueue. --- .../src/ReactFiberClassComponent.js | 23 ++++++++++----- .../react-reconciler/src/ReactUpdateQueue.js | 16 ++++++++-- packages/shared/ReactTypeOfSideEffect.js | 29 +++++++++---------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index dff6eac796973..b9ba859b148c0 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {LegacyContext} from './ReactFiberContext'; -import {Update, Snapshot, ForceUpdate} from 'shared/ReactTypeOfSideEffect'; +import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect'; import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, @@ -255,8 +255,8 @@ export default function( const expirationTime = computeExpirationForFiber(fiber); const update = createUpdate(expirationTime); - update.process = (workInProgress, prevState) => { - workInProgress.effectTag |= ForceUpdate; + update.process = (workInProgress, prevState, queue) => { + queue.hasForceUpdate = true; return prevState; }; @@ -283,8 +283,11 @@ export default function( newState, newContext, ) { - if (workInProgress.effectTag & ForceUpdate) { - // If the workInProgress already has an Update effect, return true + if ( + workInProgress.updateQueue !== null && + workInProgress.updateQueue.hasForceUpdate + ) { + // If forceUpdate was called, disregard sCU. return true; } @@ -843,7 +846,10 @@ export default function( oldProps === newProps && oldState === newState && !hasContextChanged() && - !(workInProgress.effectTag & ForceUpdate) + !( + workInProgress.updateQueue !== null && + workInProgress.updateQueue.hasForceUpdate + ) ) { // 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. @@ -971,7 +977,10 @@ export default function( oldProps === newProps && oldState === newState && !hasContextChanged() && - !(workInProgress.effectTag & ForceUpdate) + !( + workInProgress.updateQueue !== null && + workInProgress.updateQueue.hasForceUpdate + ) ) { // 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. diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 65c34eea4a8ad..187fd26301335 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -116,7 +116,13 @@ import warning from 'fbjs/lib/warning'; export type Update = { expirationTime: ExpirationTime, - process: ((workInProgress: Fiber, prevState: State) => State) | null, + process: + | (( + workInProgress: Fiber, + prevState: State, + queue: UpdateQueue, + ) => State) + | null, commit: ((finishedWork: Fiber) => mixed) | null, next: Update | null, @@ -139,6 +145,9 @@ export type UpdateQueue = { firstEffect: Update | null, lastEffect: Update | null, + // TODO: Workaround for lack of tuples. Could global state instead. + hasForceUpdate: boolean, + // DEV-only isProcessing?: boolean, }; @@ -160,6 +169,7 @@ function createUpdateQueue(baseState: State): UpdateQueue { lastCapturedUpdate: null, firstEffect: null, lastEffect: null, + hasForceUpdate: false, }; if (__DEV__) { queue.isProcessing = false; @@ -185,6 +195,8 @@ function cloneUpdateQueue( firstCapturedUpdate: null, lastCapturedUpdate: null, + hasForceUpdate: false, + firstEffect: null, lastEffect: null, }; @@ -414,7 +426,7 @@ function processSingleUpdate( addToEffectList(queue, update); } if (process !== null) { - return process(workInProgress, prevState); + return process(workInProgress, prevState, queue); } else { return prevState; } diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 19699f5749096..53b788eb5a341 100644 --- a/packages/shared/ReactTypeOfSideEffect.js +++ b/packages/shared/ReactTypeOfSideEffect.js @@ -10,23 +10,22 @@ export type TypeOfSideEffect = number; // Don't change these two values. They're used by React Dev Tools. -export const NoEffect = /* */ 0b000000000000; -export const PerformedWork = /* */ 0b000000000001; +export const NoEffect = /* */ 0b00000000000; +export const PerformedWork = /* */ 0b00000000001; // You can change the rest (and add more). -export const Placement = /* */ 0b000000000010; -export const Update = /* */ 0b000000000100; -export const PlacementAndUpdate = /* */ 0b000000000110; -export const Deletion = /* */ 0b000000001000; -export const ContentReset = /* */ 0b000000010000; -export const UpdateQueue = /* */ 0b000000100000; -export const DidCapture = /* */ 0b000001000000; -export const Ref = /* */ 0b000010000000; -export const Snapshot = /* */ 0b000100000000; -export const ForceUpdate = /* */ 0b001000000000; +export const Placement = /* */ 0b00000000010; +export const Update = /* */ 0b00000000100; +export const PlacementAndUpdate = /* */ 0b00000000110; +export const Deletion = /* */ 0b00000001000; +export const ContentReset = /* */ 0b00000010000; +export const UpdateQueue = /* */ 0b00000100000; +export const DidCapture = /* */ 0b00001000000; +export const Ref = /* */ 0b00010000000; +export const Snapshot = /* */ 0b00100000000; // Union of all host effects -export const HostEffectMask = /* */ 0b001111111111; +export const HostEffectMask = /* */ 0b00111111111; -export const Incomplete = /* */ 0b010000000000; -export const ShouldCapture = /* */ 0b100000000000; +export const Incomplete = /* */ 0b01000000000; +export const ShouldCapture = /* */ 0b10000000000; From 712816d79cb48bda69f5ba0f3112b57a68dbf607 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 19 Apr 2018 17:30:39 -0700 Subject: [PATCH 05/10] Rename UpdateQueue effect type back to Callback I don't love this name either, but it's less confusing than UpdateQueue I suppose. Conceptually, this is usually a callback: setState callbacks, componentDidCatch. The only case that feels a bit weird is Timeouts, which use this effect to attach a promise listener. I guess that kinda fits, too. --- packages/react-reconciler/src/ReactFiberScheduler.js | 4 ++-- packages/react-reconciler/src/ReactUpdateQueue.js | 8 ++++---- packages/shared/ReactTypeOfSideEffect.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 646c47c75b00c..52fbf9729d5f1 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -26,7 +26,7 @@ import { PlacementAndUpdate, Deletion, ContentReset, - UpdateQueue as UpdateQueueEffect, + Callback, ShouldCapture, Ref, Incomplete, @@ -434,7 +434,7 @@ export default function( while (nextEffect !== null) { const effectTag = nextEffect.effectTag; - if (effectTag & (Update | UpdateQueueEffect)) { + if (effectTag & (Update | Callback)) { recordEffect(); const current = nextEffect.alternate; commitLifeCycles( diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 187fd26301335..1046cfa3f2ad0 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -108,7 +108,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import {NoWork} from './ReactFiberExpirationTime'; -import {UpdateQueue as UpdateQueueEffect} from 'shared/ReactTypeOfSideEffect'; +import {Callback} from 'shared/ReactTypeOfSideEffect'; import {ClassComponent} from 'shared/ReactTypeOfWork'; import warning from 'fbjs/lib/warning'; @@ -422,7 +422,7 @@ function processSingleUpdate( const commit = update.commit; const process = update.process; if (commit !== null) { - workInProgress.effectTag |= UpdateQueueEffect; + workInProgress.effectTag |= Callback; addToEffectList(queue, update); } if (process !== null) { @@ -590,12 +590,12 @@ export function processUpdateQueue( if (newFirstRenderPhaseUpdate === null) { queue.lastRenderPhaseUpdate = null; } else { - workInProgress.effectTag |= UpdateQueueEffect; + workInProgress.effectTag |= Callback; } if (newFirstCapturedUpdate === null) { queue.lastCapturedUpdate = null; } else { - workInProgress.effectTag |= UpdateQueueEffect; + workInProgress.effectTag |= Callback; } if ( newFirstUpdate === null && diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 53b788eb5a341..27d6aa6090e45 100644 --- a/packages/shared/ReactTypeOfSideEffect.js +++ b/packages/shared/ReactTypeOfSideEffect.js @@ -19,7 +19,7 @@ export const Update = /* */ 0b00000000100; export const PlacementAndUpdate = /* */ 0b00000000110; export const Deletion = /* */ 0b00000001000; export const ContentReset = /* */ 0b00000010000; -export const UpdateQueue = /* */ 0b00000100000; +export const Callback = /* */ 0b00000100000; export const DidCapture = /* */ 0b00001000000; export const Ref = /* */ 0b00010000000; export const Snapshot = /* */ 0b00100000000; From febf9fc9eb644ace90d50ebcb32313bda15878a7 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 20 Apr 2018 15:19:06 -0700 Subject: [PATCH 06/10] Call getDerivedStateFromProps every render, even if props did not change Rather than enqueue a new setState updater for every props change, we can skip the update queue entirely and merge the result into state at the end. This makes more sense, since "receiving props" is not an event that should be observed. It's still a bit weird, since eventually we do persist the derived state (in other words, it accumulates). --- .../createSubscription-test.internal.js | 14 +- .../src/ReactFiberBeginWork.js | 14 +- .../src/ReactFiberClassComponent.js | 119 ++++++++------- .../react-reconciler/src/ReactUpdateQueue.js | 142 +----------------- .../ReactIncremental-test.internal.js | 10 +- 5 files changed, 84 insertions(+), 215 deletions(-) diff --git a/packages/create-subscription/src/__tests__/createSubscription-test.internal.js b/packages/create-subscription/src/__tests__/createSubscription-test.internal.js index 2cc81b696dd57..d96f57ba51ed6 100644 --- a/packages/create-subscription/src/__tests__/createSubscription-test.internal.js +++ b/packages/create-subscription/src/__tests__/createSubscription-test.internal.js @@ -264,7 +264,6 @@ describe('createSubscription', () => { it('should ignore values emitted by a new subscribable until the commit phase', () => { const log = []; - let parentInstance; function Child({value}) { ReactNoop.yield('Child: ' + value); @@ -301,8 +300,6 @@ describe('createSubscription', () => { } render() { - parentInstance = this; - return ( {(value = 'default') => { @@ -331,8 +328,8 @@ describe('createSubscription', () => { observableB.next('b-2'); observableB.next('b-3'); - // Mimic a higher-priority interruption - parentInstance.setState({observed: observableA}); + // Update again + ReactNoop.render(); // Flush everything and ensure that the correct subscribable is used // We expect the last emitted update to be rendered (because of the commit phase value check) @@ -354,7 +351,6 @@ describe('createSubscription', () => { it('should not drop values emitted between updates', () => { const log = []; - let parentInstance; function Child({value}) { ReactNoop.yield('Child: ' + value); @@ -391,8 +387,6 @@ describe('createSubscription', () => { } render() { - parentInstance = this; - return ( {(value = 'default') => { @@ -420,8 +414,8 @@ describe('createSubscription', () => { observableA.next('a-1'); observableA.next('a-2'); - // Mimic a higher-priority interruption - parentInstance.setState({observed: observableA}); + // Update again + ReactNoop.render(); // Flush everything and ensure that the correct subscribable is used // We expect the new subscribable to finish rendering, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 27e01836c0525..b19171eb763e7 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -55,14 +55,14 @@ import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {cancelWorkTimer} from './ReactDebugFiberPerf'; import ReactFiberClassComponent, { - createGetDerivedStateFromPropsUpdate, + applyDerivedStateFromProps, } from './ReactFiberClassComponent'; import { mountChildFibers, reconcileChildFibers, cloneChildFibers, } from './ReactChildFiber'; -import {enqueueRenderPhaseUpdate, processUpdateQueue} from './ReactUpdateQueue'; +import {processUpdateQueue} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -592,15 +592,11 @@ export default function( const getDerivedStateFromProps = Component.getDerivedStateFromProps; if (typeof getDerivedStateFromProps === 'function') { - const update = createGetDerivedStateFromPropsUpdate( + applyDerivedStateFromProps( + workInProgress, getDerivedStateFromProps, - renderExpirationTime, + props, ); - enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); - } } // Push context providers early to prevent context stack mismatches. diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index b9ba859b148c0..3fc53a4335453 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -30,10 +30,10 @@ import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; import { enqueueUpdate, - enqueueRenderPhaseUpdate, processUpdateQueue, createUpdate, } from './ReactUpdateQueue'; +import {NoWork} from './ReactFiberExpirationTime'; const fakeInternalInstance = {}; const isArray = Array.isArray; @@ -109,32 +109,40 @@ if (__DEV__) { Object.freeze(fakeInternalInstance); } -export function createGetDerivedStateFromPropsUpdate( +export function applyDerivedStateFromProps( + workInProgress: Fiber, getDerivedStateFromProps: (props: any, state: any) => any, - renderExpirationTime: ExpirationTime, + nextProps: any, ) { - const update = createUpdate(renderExpirationTime); - update.process = (nextWorkInProgress, prevState) => { - const nextProps = nextWorkInProgress.pendingProps; + const prevState = workInProgress.memoizedState; - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - nextWorkInProgress.mode & StrictMode) - ) { - // Invoke the function an extra time to help detect side-effects. - getDerivedStateFromProps(nextProps, prevState); - } + 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); + const partialState = getDerivedStateFromProps(nextProps, prevState); - if (__DEV__) { - warnOnUndefinedDerivedState(nextWorkInProgress, partialState); - } - // Merge the partial state and the previous state. - return Object.assign({}, prevState, partialState); - }; - return update; + if (__DEV__) { + warnOnUndefinedDerivedState(workInProgress, partialState); + } + // Merge the partial state and the previous state. + const memoizedState = + partialState === null || partialState === undefined + ? prevState + : Object.assign({}, prevState, partialState); + workInProgress.memoizedState = memoizedState; + + // Once the update queue is empty, persist the derived state onto the + // base state. + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null && updateQueue.expirationTime === NoWork) { + updateQueue.baseState = memoizedState; + } } export default function( @@ -742,19 +750,20 @@ export default function( } } + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + instance.state = workInProgress.memoizedState; + } + const getDerivedStateFromProps = workInProgress.type.getDerivedStateFromProps; if (typeof getDerivedStateFromProps === 'function') { - const update = createGetDerivedStateFromPropsUpdate( + applyDerivedStateFromProps( + workInProgress, getDerivedStateFromProps, - renderExpirationTime, + props, ); - enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); - } - - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); instance.state = workInProgress.memoizedState; } @@ -796,8 +805,9 @@ export default function( const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = - typeof ctor.getDerivedStateFromProps === 'function' || + typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function'; // Note: During these life-cycles, instance.props/instance.state are what @@ -821,19 +831,6 @@ export default function( } } - // Only call getDerivedStateFromProps if the props have changed - if (oldProps !== newProps) { - const getDerivedStateFromProps = - workInProgress.type.getDerivedStateFromProps; - if (typeof getDerivedStateFromProps === 'function') { - const update = createGetDerivedStateFromPropsUpdate( - getDerivedStateFromProps, - renderExpirationTime, - ); - enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); - } - } - const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; @@ -842,6 +839,15 @@ export default function( newState = workInProgress.memoizedState; } + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + if ( oldProps === newProps && oldState === newState && @@ -927,8 +933,9 @@ export default function( const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = - typeof ctor.getDerivedStateFromProps === 'function' || + typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function'; // Note: During these life-cycles, instance.props/instance.state are what @@ -952,19 +959,6 @@ export default function( } } - // Only call getDerivedStateFromProps if the props have changed - if (oldProps !== newProps) { - const getDerivedStateFromProps = - workInProgress.type.getDerivedStateFromProps; - if (typeof getDerivedStateFromProps === 'function') { - const update = createGetDerivedStateFromPropsUpdate( - getDerivedStateFromProps, - renderExpirationTime, - ); - enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); - } - } - const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; @@ -973,6 +967,15 @@ export default function( newState = workInProgress.memoizedState; } + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + newProps, + ); + newState = workInProgress.memoizedState; + } + if ( oldProps === newProps && oldState === newState && diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 1046cfa3f2ad0..3938a95b1188e 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -83,26 +83,6 @@ // 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 {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -136,9 +116,6 @@ export type UpdateQueue = { firstUpdate: Update | null, lastUpdate: Update | null, - firstRenderPhaseUpdate: Update | null, - lastRenderPhaseUpdate: Update | null, - firstCapturedUpdate: Update | null, lastCapturedUpdate: Update | null, @@ -157,14 +134,12 @@ if (__DEV__) { didWarnUpdateInsideUpdate = false; } -function createUpdateQueue(baseState: State): UpdateQueue { +export function createUpdateQueue(baseState: State): UpdateQueue { const queue: UpdateQueue = { expirationTime: NoWork, baseState, firstUpdate: null, lastUpdate: null, - firstRenderPhaseUpdate: null, - lastRenderPhaseUpdate: null, firstCapturedUpdate: null, lastCapturedUpdate: null, firstEffect: null, @@ -186,10 +161,6 @@ function cloneUpdateQueue( firstUpdate: currentQueue.firstUpdate, lastUpdate: currentQueue.lastUpdate, - // These are only valid for the lifetime of a single work-in-progress. - firstRenderPhaseUpdate: null, - lastRenderPhaseUpdate: null, - // TODO: With resuming, if we bail out and resuse the child tree, we should // keep these effects. firstCapturedUpdate: null, @@ -319,46 +290,6 @@ export function enqueueUpdate( } } -export function enqueueRenderPhaseUpdate( - workInProgress: Fiber, - update: Update, - renderExpirationTime: ExpirationTime, -) { - // Render phase updates go into a separate list, and only on the work-in- - // progress queue. - let workInProgressQueue = workInProgress.updateQueue; - if (workInProgressQueue === null) { - workInProgressQueue = workInProgress.updateQueue = createUpdateQueue( - workInProgress.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( - workInProgress, - 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; - } -} - export function enqueueCapturedUpdate( workInProgress: Fiber, update: Update, @@ -508,44 +439,6 @@ export function processUpdateQueue( 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. - newFirstRenderPhaseUpdate = 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( - workInProgress, - queue, - update, - resultState, - ); - } - update = update.next; - } - // Separately, iterate though the list of captured updates. let newFirstCapturedUpdate = null; update = queue.firstCapturedUpdate; @@ -559,7 +452,7 @@ export function processUpdateQueue( newFirstCapturedUpdate = update; // If this is the first update that was skipped, the current result is // the new base state. - if (newFirstUpdate === null && newFirstRenderPhaseUpdate === null) { + if (newFirstUpdate === null) { newBaseState = resultState; } } @@ -587,21 +480,12 @@ export function processUpdateQueue( if (newFirstUpdate === null) { queue.lastUpdate = null; } - if (newFirstRenderPhaseUpdate === null) { - queue.lastRenderPhaseUpdate = null; - } else { - workInProgress.effectTag |= Callback; - } if (newFirstCapturedUpdate === null) { queue.lastCapturedUpdate = null; } else { workInProgress.effectTag |= Callback; } - if ( - newFirstUpdate === null && - newFirstRenderPhaseUpdate === null && - newFirstCapturedUpdate === null - ) { + if (newFirstUpdate === null && newFirstCapturedUpdate === null) { // We processed every update, without skipping. That means the new base // state is the same as the result state. newBaseState = resultState; @@ -609,7 +493,6 @@ export function processUpdateQueue( queue.baseState = newBaseState; queue.firstUpdate = newFirstUpdate; - queue.firstRenderPhaseUpdate = newFirstRenderPhaseUpdate; queue.firstCapturedUpdate = newFirstCapturedUpdate; queue.expirationTime = newExpirationTime; @@ -625,28 +508,17 @@ export function commitUpdateQueue( finishedQueue: UpdateQueue, renderExpirationTime: ExpirationTime, ): 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 + // If the finished render included captured updates, and there are still + // lower priority updates left over, we need to keep the captured 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) { - finishedQueue.lastUpdate.next = finishedQueue.firstRenderPhaseUpdate; - finishedQueue.lastUpdate = finishedQueue.lastRenderPhaseUpdate; - } - // Clear the list of render phase updates. - finishedQueue.firstRenderPhaseUpdate = finishedQueue.lastRenderPhaseUpdate = null; - } - - // Same with captured updates if (finishedQueue.firstCapturedUpdate !== null) { - // Join the render phase update list to the end of the normal list. + // Join the captured update list to the end of the normal list. if (finishedQueue.lastUpdate !== null) { finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate; finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate; } - // Clear the list of render phase updates. + // Clear the list of captured updates. finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null; } diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index e9f22dec8c16f..a8b66373d8f48 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1422,7 +1422,7 @@ describe('ReactIncremental', () => { ]); }); - it('does not call static getDerivedStateFromProps for state-only updates', () => { + it('calls getDerivedStateFromProps even for state-only updates', () => { let ops = []; let instance; @@ -1456,8 +1456,12 @@ describe('ReactIncremental', () => { instance.changeState(); ReactNoop.flush(); - expect(ops).toEqual(['render', 'componentDidUpdate']); - expect(instance.state).toEqual({foo: 'bar'}); + expect(ops).toEqual([ + 'getDerivedStateFromProps', + 'render', + 'componentDidUpdate', + ]); + expect(instance.state).toEqual({foo: 'foo'}); }); xit('does not call componentWillReceiveProps for state-only updates', () => { From 3c0c540933b0dcc5b355ae7f2ae90d4b8e285dc6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 20 Apr 2018 15:27:49 -0700 Subject: [PATCH 07/10] Store captured effects on separate list from "own" effects (callbacks) For resuming, we need the ability to discard the "own" effects while reusing the captured effects. --- .../react-reconciler/src/ReactUpdateQueue.js | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 3938a95b1188e..7376e216d5804 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -122,7 +122,10 @@ export type UpdateQueue = { firstEffect: Update | null, lastEffect: Update | null, - // TODO: Workaround for lack of tuples. Could global state instead. + firstCapturedEffect: Update | null, + lastCapturedEffect: Update | null, + + // TODO: Workaround for lack of tuples. Could use global state instead. hasForceUpdate: boolean, // DEV-only @@ -144,6 +147,8 @@ export function createUpdateQueue(baseState: State): UpdateQueue { lastCapturedUpdate: null, firstEffect: null, lastEffect: null, + firstCapturedEffect: null, + lastCapturedEffect: null, hasForceUpdate: false, }; if (__DEV__) { @@ -170,6 +175,9 @@ function cloneUpdateQueue( firstEffect: null, lastEffect: null, + + firstCapturedEffect: null, + lastCapturedEffect: null, }; if (__DEV__) { queue.isProcessing = false; @@ -330,39 +338,6 @@ export function enqueueCapturedUpdate( } } -function addToEffectList( - queue: UpdateQueue, - update: 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 processSingleUpdate( - workInProgress: Fiber, - queue: UpdateQueue, - update: Update, - prevState: State, -): State { - const commit = update.commit; - const process = update.process; - if (commit !== null) { - workInProgress.effectTag |= Callback; - addToEffectList(queue, update); - } - if (process !== null) { - return process(workInProgress, prevState, queue); - } else { - return prevState; - } -} - function ensureWorkInProgressQueueIsAClone( workInProgress: Fiber, queue: UpdateQueue, @@ -428,12 +403,22 @@ export function processUpdateQueue( } else { // This update does have sufficient priority. Process it and compute // a new result. - resultState = processSingleUpdate( - workInProgress, - queue, - update, - resultState, - ); + const commit = update.commit; + const process = update.process; + if (process !== null) { + resultState = process(workInProgress, resultState, queue); + } + if (commit !== null) { + workInProgress.effectTag |= Callback; + // 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; + } + } } // Continue to the next update. update = update.next; @@ -467,12 +452,22 @@ export function processUpdateQueue( } else { // This update does have sufficient priority. Process it and compute // a new result. - resultState = processSingleUpdate( - workInProgress, - queue, - update, - resultState, - ); + const commit = update.commit; + const process = update.process; + if (process !== null) { + resultState = process(workInProgress, resultState, queue); + } + if (commit !== null) { + workInProgress.effectTag |= Callback; + // Set this to null, in case it was mutated during an aborted render. + update.nextEffect = null; + if (queue.lastCapturedEffect === null) { + queue.firstCapturedEffect = queue.lastCapturedEffect = update; + } else { + queue.lastCapturedEffect.nextEffect = update; + queue.lastCapturedEffect = update; + } + } } update = update.next; } @@ -533,4 +528,15 @@ export function commitUpdateQueue( } effect = effect.nextEffect; } + + effect = finishedQueue.firstCapturedEffect; + finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null; + while (effect !== null) { + const commit = effect.commit; + if (commit !== null) { + effect.commit = null; + commit(finishedWork); + } + effect = effect.nextEffect; + } } From 5be461c02bc21f9e31fa55a34729b90812168fe1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 20 Apr 2018 19:49:06 -0700 Subject: [PATCH 08/10] Optimize for class components Change `process` and `callback` to match the expected payload types for class components. I had intended for the update queue to be reusable for both class components and a future React API, but we'll likely have to fork anyway. --- .../src/ReactFiberBeginWork.js | 15 +- .../src/ReactFiberClassComponent.js | 120 +++++-------- .../src/ReactFiberCommitWork.js | 27 ++- .../src/ReactFiberReconciler.js | 26 +-- .../src/ReactFiberScheduler.js | 16 +- .../src/ReactFiberUnwindWork.js | 92 ++++------ .../react-reconciler/src/ReactUpdateQueue.js | 159 ++++++++++++++---- 7 files changed, 254 insertions(+), 201 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index b19171eb763e7..24e3245eab9e8 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -411,9 +411,18 @@ export default function( pushHostRootContext(workInProgress); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - const prevChildren = workInProgress.memoizedState; - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); - const nextChildren = workInProgress.memoizedState; + const nextProps = workInProgress.pendingProps; + const prevState = workInProgress.memoizedState; + const prevChildren = prevState !== null ? prevState.children : null; + processUpdateQueue( + workInProgress, + updateQueue, + nextProps, + null, + renderExpirationTime, + ); + const nextState = workInProgress.memoizedState; + const nextChildren = nextState.children; if (nextChildren === prevChildren) { // If the state is the same as before, that's a bailout because we had diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 3fc53a4335453..e76b2b313dfb8 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -32,6 +32,8 @@ import { enqueueUpdate, processUpdateQueue, createUpdate, + ReplaceState, + ForceUpdate, } from './ReactUpdateQueue'; import {NoWork} from './ReactFiberExpirationTime'; @@ -160,16 +162,6 @@ export default function( hasContextChanged, } = legacyContext; - function callCallback(callback, context) { - invariant( - typeof callback === 'function', - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: %s', - callback, - ); - callback.call(context); - } - const classComponentUpdater = { isMounted, enqueueSetState(inst, payload, callback) { @@ -177,43 +169,12 @@ export default function( const expirationTime = computeExpirationForFiber(fiber); const update = createUpdate(expirationTime); - update.process = (workInProgress, prevState) => { - 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); - }; - + update.payload = payload; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - update.commit = finishedWork => { - const instance = finishedWork.stateNode; - callCallback(callback, instance); - }; + update.callback = callback; } enqueueUpdate(fiber, update, expirationTime); @@ -224,35 +185,14 @@ export default function( const expirationTime = computeExpirationForFiber(fiber); const update = createUpdate(expirationTime); - update.process = (workInProgress, prevState) => { - 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; - }; + update.tag = ReplaceState; + update.payload = payload; if (callback !== undefined && callback !== null) { if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); + warnOnInvalidCallback(callback, 'replaceState'); } - update.commit = finishedWork => { - const instance = finishedWork.stateNode; - callCallback(callback, instance); - }; + update.callback = callback; } enqueueUpdate(fiber, update, expirationTime); @@ -263,19 +203,13 @@ export default function( const expirationTime = computeExpirationForFiber(fiber); const update = createUpdate(expirationTime); - update.process = (workInProgress, prevState, queue) => { - queue.hasForceUpdate = true; - return prevState; - }; + update.tag = ForceUpdate; if (callback !== undefined && callback !== null) { if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); + warnOnInvalidCallback(callback, 'forceUpdate'); } - update.commit = finishedWork => { - const instance = finishedWork.stateNode; - callCallback(callback, instance); - }; + update.callback = callback; } enqueueUpdate(fiber, update, expirationTime); @@ -752,7 +686,13 @@ export default function( let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + processUpdateQueue( + workInProgress, + updateQueue, + props, + instance, + renderExpirationTime, + ); instance.state = workInProgress.memoizedState; } @@ -780,7 +720,13 @@ export default function( // process them now. updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + processUpdateQueue( + workInProgress, + updateQueue, + props, + instance, + renderExpirationTime, + ); instance.state = workInProgress.memoizedState; } } @@ -835,7 +781,13 @@ export default function( let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + processUpdateQueue( + workInProgress, + updateQueue, + newProps, + instance, + renderExpirationTime, + ); newState = workInProgress.memoizedState; } @@ -963,7 +915,13 @@ export default function( let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + processUpdateQueue( + workInProgress, + updateQueue, + newProps, + instance, + renderExpirationTime, + ); newState = workInProgress.memoizedState; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index e2195e01c4d1d..a3754902975c1 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -251,14 +251,37 @@ export default function( } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitUpdateQueue(finishedWork, updateQueue, committedExpirationTime); + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } return; } case HostRoot: { const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitUpdateQueue(finishedWork, updateQueue, committedExpirationTime); + 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; + } + } + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } return; } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index f71773162150c..519003f17719a 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -17,7 +17,7 @@ import { findCurrentHostFiberWithNoPortals, } from 'react-reconciler/reflection'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; -import {ClassComponent, HostComponent} from 'shared/ReactTypeOfWork'; +import {HostComponent} from 'shared/ReactTypeOfWork'; import emptyObject from 'fbjs/lib/emptyObject'; import getComponentName from 'shared/getComponentName'; import invariant from 'fbjs/lib/invariant'; @@ -340,8 +340,7 @@ export default function( } const update = createUpdate(expirationTime); - - update.process = () => element; + update.payload = {children: element}; callback = callback === undefined ? null : callback; if (callback !== null) { @@ -351,26 +350,7 @@ export default function( 'function. Instead received: %s.', callback, ); - update.commit = finishedWork => { - 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; - } - } - invariant( - typeof callback === 'function', - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: %s', - callback, - ); - callback.call(instance); - }; + update.callback = callback; } enqueueUpdate(current, update, expirationTime); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 52fbf9729d5f1..708fa9e12a6e4 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -27,7 +27,7 @@ import { Deletion, ContentReset, Callback, - ShouldCapture, + DidCapture, Ref, Incomplete, HostEffectMask, @@ -803,7 +803,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 & ShouldCapture) { + if (workInProgress.effectTag & DidCapture) { // Restarting an error boundary stopFailedWorkTimer(workInProgress); } else { @@ -1065,7 +1065,11 @@ export default function( break; case HostRoot: { const errorInfo = createCapturedValue(value, sourceFiber); - const update = createRootErrorUpdate(errorInfo, expirationTime); + const update = createRootErrorUpdate( + fiber, + errorInfo, + expirationTime, + ); enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); return; @@ -1079,7 +1083,11 @@ export default function( // itself should capture it. const rootFiber = sourceFiber; const errorInfo = createCapturedValue(value, rootFiber); - const update = createRootErrorUpdate(errorInfo, expirationTime); + const update = createRootErrorUpdate( + rootFiber, + errorInfo, + expirationTime, + ); enqueueUpdate(rootFiber, update, expirationTime); scheduleWork(rootFiber, expirationTime); } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 1923dd7f1cdb6..e59324c3c4239 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -16,7 +16,11 @@ import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {enqueueCapturedUpdate, createUpdate} from './ReactUpdateQueue'; +import { + enqueueCapturedUpdate, + createUpdate, + CaptureUpdate, +} from './ReactUpdateQueue'; import {logError} from './ReactFiberCommitWork'; import { @@ -32,13 +36,8 @@ import { Incomplete, ShouldCapture, } from 'shared/ReactTypeOfSideEffect'; -import {StrictMode} from './ReactTypeOfMode'; -import { - enableGetDerivedStateFromCatch, - debugRenderPhaseSideEffects, - debugRenderPhaseSideEffectsForStrictMode, -} from 'shared/ReactFeatureFlags'; +import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags'; export default function( hostContext: HostContext, @@ -61,18 +60,18 @@ export default function( const {popProvider} = newContext; function createRootErrorUpdate( + fiber: Fiber, errorInfo: CapturedValue, expirationTime: ExpirationTime, ): Update { const update = createUpdate(expirationTime); - update.process = nextWorkInProgress => { - // Unmount the root by rendering null. - return null; - }; - update.commit = finishedWork => { - const error = errorInfo.value; + // Unmount the root by rendering null. + update.tag = CaptureUpdate; + update.payload = {children: null}; + const error = errorInfo.value; + update.callback = () => { onUncaughtError(error); - logError(finishedWork, errorInfo); + logError(fiber, errorInfo); }; return update; } @@ -83,61 +82,36 @@ export default function( expirationTime: ExpirationTime, ): Update { const update = createUpdate(expirationTime); - update.process = (workInProgress, prevState) => { - 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; - } - }; + update.tag = CaptureUpdate; + const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch; + if ( + enableGetDerivedStateFromCatch && + typeof getDerivedStateFromCatch === 'function' + ) { + const error = errorInfo.value; + update.payload = () => { + return getDerivedStateFromCatch(error); + }; + } const inst = fiber.stateNode; if (inst !== null && typeof inst.componentDidCatch === 'function') { - update.commit = finishedWork => { - const instance = finishedWork.stateNode; - const ctor = finishedWork.type; - + update.callback = function callback() { if ( !enableGetDerivedStateFromCatch || - typeof ctor.getDerivedStateFromCatch !== 'function' + 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); + markLegacyErrorBoundaryAsFailed(this); } - - instance.props = finishedWork.memoizedProps; - instance.state = finishedWork.memoizedState; const error = errorInfo.value; const stack = errorInfo.stack; - logError(finishedWork, errorInfo); - instance.componentDidCatch(error, { + logError(fiber, errorInfo); + this.componentDidCatch(error, { componentStack: stack !== null ? stack : '', }); }; @@ -164,7 +138,11 @@ export default function( case HostRoot: { const errorInfo = value; workInProgress.effectTag |= ShouldCapture; - const update = createRootErrorUpdate(errorInfo, renderExpirationTime); + const update = createRootErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } @@ -182,7 +160,6 @@ export default function( !isAlreadyFailedLegacyErrorBoundary(instance))) ) { workInProgress.effectTag |= ShouldCapture; - // Schedule the error boundary to re-render using updated state const update = createClassErrorUpdate( workInProgress, @@ -206,6 +183,7 @@ 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/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 7376e216d5804..72f1b27d5cfb7 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -88,22 +88,29 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import {NoWork} from './ReactFiberExpirationTime'; -import {Callback} from 'shared/ReactTypeOfSideEffect'; +import { + Callback, + ShouldCapture, + DidCapture, +} from 'shared/ReactTypeOfSideEffect'; import {ClassComponent} from 'shared/ReactTypeOfWork'; +import { + debugRenderPhaseSideEffects, + debugRenderPhaseSideEffectsForStrictMode, +} from 'shared/ReactFeatureFlags'; + +import {StrictMode} from './ReactTypeOfMode'; + +import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; export type Update = { expirationTime: ExpirationTime, - process: - | (( - workInProgress: Fiber, - prevState: State, - queue: UpdateQueue, - ) => State) - | null, - commit: ((finishedWork: Fiber) => mixed) | null, + tag: 0 | 1 | 2 | 3, + payload: any, + callback: (() => mixed) | null, next: Update | null, nextEffect: Update | null, @@ -132,6 +139,11 @@ export type UpdateQueue = { isProcessing?: boolean, }; +export const UpdateState = 0; +export const ReplaceState = 1; +export const ForceUpdate = 2; +export const CaptureUpdate = 3; + let didWarnUpdateInsideUpdate; if (__DEV__) { didWarnUpdateInsideUpdate = false; @@ -189,8 +201,9 @@ export function createUpdate(expirationTime: ExpirationTime): Update<*> { return { expirationTime: expirationTime, - process: null, - commit: null, + tag: UpdateState, + payload: null, + callback: null, next: null, nextEffect: null, @@ -353,9 +366,74 @@ function ensureWorkInProgressQueueIsAClone( return queue; } +function getStateFromUpdate( + workInProgress: Fiber, + queue: UpdateQueue, + update: Update, + prevState: State, + nextProps: any, + instance: any, +): any { + switch (update.tag) { + case ReplaceState: { + const payload = update.payload; + if (typeof payload === 'function') { + // Updater function + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + payload.call(instance, prevState, nextProps); + } + + return payload.call(instance, prevState, nextProps); + } + // State object + return payload; + } + case CaptureUpdate: { + workInProgress.effectTag = + (workInProgress.effectTag & ~ShouldCapture) | DidCapture; + } + // Intentional fallthrough + case UpdateState: { + const payload = update.payload; + let partialState; + if (typeof payload === 'function') { + // Updater function + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + 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: { + queue.hasForceUpdate = true; + return prevState; + } + } + return prevState; +} + export function processUpdateQueue( workInProgress: Fiber, queue: UpdateQueue, + props: any, + instance: any, renderExpirationTime: ExpirationTime, ): void { if ( @@ -403,12 +481,16 @@ export function processUpdateQueue( } else { // This update does have sufficient priority. Process it and compute // a new result. - const commit = update.commit; - const process = update.process; - if (process !== null) { - resultState = process(workInProgress, resultState, queue); - } - if (commit !== null) { + resultState = getStateFromUpdate( + workInProgress, + queue, + update, + resultState, + props, + instance, + ); + const callback = update.callback; + if (callback !== null) { workInProgress.effectTag |= Callback; // Set this to null, in case it was mutated during an aborted render. update.nextEffect = null; @@ -452,12 +534,16 @@ export function processUpdateQueue( } else { // This update does have sufficient priority. Process it and compute // a new result. - const commit = update.commit; - const process = update.process; - if (process !== null) { - resultState = process(workInProgress, resultState, queue); - } - if (commit !== null) { + resultState = getStateFromUpdate( + workInProgress, + queue, + update, + resultState, + props, + instance, + ); + const callback = update.callback; + if (callback !== null) { workInProgress.effectTag |= Callback; // Set this to null, in case it was mutated during an aborted render. update.nextEffect = null; @@ -498,9 +584,20 @@ export function processUpdateQueue( } } +function callCallback(callback, context) { + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(context); +} + export function commitUpdateQueue( finishedWork: Fiber, finishedQueue: UpdateQueue, + instance: any, renderExpirationTime: ExpirationTime, ): void { // If the finished render included captured updates, and there are still @@ -521,10 +618,10 @@ export function commitUpdateQueue( let effect = finishedQueue.firstEffect; finishedQueue.firstEffect = finishedQueue.lastEffect = null; while (effect !== null) { - const commit = effect.commit; - if (commit !== null) { - effect.commit = null; - commit(finishedWork); + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); } effect = effect.nextEffect; } @@ -532,10 +629,10 @@ export function commitUpdateQueue( effect = finishedQueue.firstCapturedEffect; finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null; while (effect !== null) { - const commit = effect.commit; - if (commit !== null) { - effect.commit = null; - commit(finishedWork); + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); } effect = effect.nextEffect; } From 891e3118b218e7fb518d019888bb675144abf686 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 22 Apr 2018 22:24:08 -0700 Subject: [PATCH 09/10] Only double-invoke render phase lifecycles functions in DEV --- .../src/ReactFiberBeginWork.js | 7 -- .../src/ReactFiberClassComponent.js | 30 ++++--- .../react-reconciler/src/ReactUpdateQueue.js | 29 ++++--- .../ReactStrictMode-test.internal.js | 82 ++++++++++++------- 4 files changed, 87 insertions(+), 61 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 24e3245eab9e8..e49a2e0a575b3 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -346,13 +346,6 @@ export default function( } ReactDebugCurrentFiber.setCurrentPhase(null); } else { - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - instance.render(); - } nextChildren = instance.render(); } } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index e76b2b313dfb8..444d1070e686d 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -118,13 +118,15 @@ export function applyDerivedStateFromProps( ) { const prevState = workInProgress.memoizedState; - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - // Invoke the function an extra time to help detect side-effects. - getDerivedStateFromProps(nextProps, prevState); + if (__DEV__) { + 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); @@ -477,12 +479,14 @@ export default function( : emptyObject; // Instantiate twice to help detect side-effects. - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - new ctor(props, context); // eslint-disable-line no-new + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + new ctor(props, context); // eslint-disable-line no-new + } } const instance = new ctor(props, context); diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 72f1b27d5cfb7..374a908d41386 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -379,14 +379,15 @@ function getStateFromUpdate( const payload = update.payload; if (typeof payload === 'function') { // Updater function - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - payload.call(instance, prevState, nextProps); + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + payload.call(instance, prevState, nextProps); + } } - return payload.call(instance, prevState, nextProps); } // State object @@ -402,12 +403,14 @@ function getStateFromUpdate( let partialState; if (typeof payload === 'function') { // Updater function - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - payload.call(instance, prevState, nextProps); + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + payload.call(instance, prevState, nextProps); + } } partialState = payload.call(instance, prevState, nextProps); } else { diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js index 5a5a799bf8c1b..9236ae7754d42 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js @@ -57,38 +57,64 @@ describe('ReactStrictMode', () => { const component = ReactTestRenderer.create(); - expect(log).toEqual([ - 'constructor', - 'constructor', - 'getDerivedStateFromProps', - 'getDerivedStateFromProps', - 'render', - 'render', - 'componentDidMount', - ]); + if (__DEV__) { + expect(log).toEqual([ + 'constructor', + 'constructor', + 'getDerivedStateFromProps', + 'getDerivedStateFromProps', + 'render', + 'render', + 'componentDidMount', + ]); + } else { + expect(log).toEqual([ + 'constructor', + 'getDerivedStateFromProps', + 'render', + 'componentDidMount', + ]); + } log = []; shouldComponentUpdate = true; component.update(); - expect(log).toEqual([ - 'getDerivedStateFromProps', - 'getDerivedStateFromProps', - 'shouldComponentUpdate', - 'render', - 'render', - 'componentDidUpdate', - ]); + if (__DEV__) { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + 'render', + 'render', + 'componentDidUpdate', + ]); + } else { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + 'render', + 'componentDidUpdate', + ]); + } log = []; shouldComponentUpdate = false; component.update(); - expect(log).toEqual([ - 'getDerivedStateFromProps', - 'getDerivedStateFromProps', - 'shouldComponentUpdate', - ]); + + if (__DEV__) { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + ]); + } else { + expect(log).toEqual([ + 'getDerivedStateFromProps', + 'shouldComponentUpdate', + ]); + } }); it('should invoke setState callbacks twice', () => { @@ -112,8 +138,8 @@ describe('ReactStrictMode', () => { }; }); - // Callback should be invoked twice - expect(setStateCount).toBe(2); + // Callback should be invoked twice in DEV + expect(setStateCount).toBe(__DEV__ ? 2 : 1); // But each time `state` should be the previous value expect(instance.state.count).toBe(2); }); @@ -174,7 +200,7 @@ describe('ReactStrictMode', () => { const component = ReactTestRenderer.create(); - if (debugRenderPhaseSideEffectsForStrictMode) { + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) { expect(log).toEqual([ 'constructor', 'constructor', @@ -197,7 +223,7 @@ describe('ReactStrictMode', () => { shouldComponentUpdate = true; component.update(); - if (debugRenderPhaseSideEffectsForStrictMode) { + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) { expect(log).toEqual([ 'getDerivedStateFromProps', 'getDerivedStateFromProps', @@ -219,7 +245,7 @@ describe('ReactStrictMode', () => { shouldComponentUpdate = false; component.update(); - if (debugRenderPhaseSideEffectsForStrictMode) { + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) { expect(log).toEqual([ 'getDerivedStateFromProps', 'getDerivedStateFromProps', @@ -263,7 +289,7 @@ describe('ReactStrictMode', () => { // Callback should be invoked twice (in DEV) expect(setStateCount).toBe( - debugRenderPhaseSideEffectsForStrictMode ? 2 : 1, + __DEV__ && debugRenderPhaseSideEffectsForStrictMode ? 2 : 1, ); // But each time `state` should be the previous value expect(instance.state.count).toBe(2); From e4ce36312a47a83c79bce470e31b9c39e9af708c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 22 Apr 2018 22:35:08 -0700 Subject: [PATCH 10/10] Use global state to track currently processing queue in DEV --- .../src/ReactFiberScheduler.js | 8 ++++++- .../react-reconciler/src/ReactUpdateQueue.js | 22 +++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 708fa9e12a6e4..1a9846b0bdbaf 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -88,7 +88,7 @@ import { import {AsyncMode} from './ReactTypeOfMode'; import ReactFiberLegacyContext from './ReactFiberContext'; import ReactFiberNewContext from './ReactFiberNewContext'; -import {enqueueUpdate} from './ReactUpdateQueue'; +import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; @@ -960,6 +960,12 @@ export default function( break; } + if (__DEV__) { + // Reset global debug state + // We assume this is defined in DEV + (resetCurrentlyProcessingQueue: any)(); + } + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { const failedUnitOfWork = nextUnitOfWork; replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync); diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 374a908d41386..574a3c07406d9 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -134,9 +134,6 @@ export type UpdateQueue = { // TODO: Workaround for lack of tuples. Could use global state instead. hasForceUpdate: boolean, - - // DEV-only - isProcessing?: boolean, }; export const UpdateState = 0; @@ -145,8 +142,14 @@ export const ForceUpdate = 2; export const CaptureUpdate = 3; let didWarnUpdateInsideUpdate; +let currentlyProcessingQueue; +export let resetCurrentlyProcessingQueue; if (__DEV__) { didWarnUpdateInsideUpdate = false; + currentlyProcessingQueue = null; + resetCurrentlyProcessingQueue = () => { + currentlyProcessingQueue = null; + }; } export function createUpdateQueue(baseState: State): UpdateQueue { @@ -163,9 +166,6 @@ export function createUpdateQueue(baseState: State): UpdateQueue { lastCapturedEffect: null, hasForceUpdate: false, }; - if (__DEV__) { - queue.isProcessing = false; - } return queue; } @@ -191,9 +191,6 @@ function cloneUpdateQueue( firstCapturedEffect: null, lastCapturedEffect: null, }; - if (__DEV__) { - queue.isProcessing = false; - } return queue; } @@ -296,7 +293,8 @@ export function enqueueUpdate( if (__DEV__) { if ( fiber.tag === ClassComponent && - (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && + (currentlyProcessingQueue === queue1 || + (queue2 !== null && currentlyProcessingQueue === queue2)) && !didWarnUpdateInsideUpdate ) { warning( @@ -450,7 +448,7 @@ export function processUpdateQueue( queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); if (__DEV__) { - queue.isProcessing = true; + currentlyProcessingQueue = queue; } // These values may change as we process the queue. @@ -583,7 +581,7 @@ export function processUpdateQueue( workInProgress.memoizedState = resultState; if (__DEV__) { - queue.isProcessing = false; + currentlyProcessingQueue = null; } }