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-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index 7b7bb61d0dd3c..5cd6df0fbd29b 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'; @@ -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/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 f2d93b09625bb..f2a8ad1f2fe91 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -36,10 +36,12 @@ import { ContextConsumer, } from 'shared/ReactTypeOfWork'; import { + NoEffect, PerformedWork, Placement, ContentReset, Ref, + DidCapture, } from 'shared/ReactTypeOfSideEffect'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import { @@ -53,13 +55,15 @@ import warning from 'fbjs/lib/warning'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {cancelWorkTimer} from './ReactDebugFiberPerf'; -import ReactFiberClassComponent from './ReactFiberClassComponent'; +import ReactFiberClassComponent, { + applyDerivedStateFromProps, +} from './ReactFiberClassComponent'; import { mountChildFibers, reconcileChildFibers, cloneChildFibers, } from './ReactChildFiber'; -import {processUpdateQueue} from './ReactFiberUpdateQueue'; +import {processUpdateQueue} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -108,7 +112,6 @@ export default function( const { adoptClassInstance, - callGetDerivedStateFromProps, constructClassInstance, mountClassInstance, resumeMountClassInstance, @@ -263,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; @@ -281,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, ); } @@ -306,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) { @@ -351,13 +349,6 @@ export default function( } ReactDebugCurrentFiber.setCurrentPhase(null); } else { - if ( - debugRenderPhaseSideEffects || - (debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode) - ) { - instance.render(); - } nextChildren = instance.render(); } } @@ -416,29 +407,24 @@ export default function( pushHostRootContext(workInProgress); let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { + const nextProps = workInProgress.pendingProps; const prevState = workInProgress.memoizedState; - const state = processUpdateQueue( - current, + const prevChildren = prevState !== null ? prevState.children : null; + processUpdateQueue( workInProgress, updateQueue, - null, + nextProps, 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 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 // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current, workInProgress); - } else { - element = state.element; } const root: FiberRoot = workInProgress.stateNode; if ( @@ -463,16 +449,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(); @@ -610,21 +595,13 @@ export default function( workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null; - if (typeof Component.getDerivedStateFromProps === 'function') { - const partialState = callGetDerivedStateFromProps( + const getDerivedStateFromProps = Component.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( workInProgress, - value, + getDerivedStateFromProps, props, - workInProgress.memoizedState, ); - - if (partialState !== null && partialState !== undefined) { - workInProgress.memoizedState = Object.assign( - {}, - workInProgress.memoizedState, - partialState, - ); - } } // Push context providers early to prevent context stack mismatches. @@ -638,7 +615,6 @@ export default function( workInProgress, true, hasContext, - false, renderExpirationTime, ); } else { diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 3f811c11088a3..444d1070e686d 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 { - enableGetDerivedStateFromCatch, debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, warnAboutDeprecatedLifecycles, @@ -31,26 +29,31 @@ import warning from 'fbjs/lib/warning'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; import { - insertUpdateIntoFiber, + enqueueUpdate, processUpdateQueue, -} from './ReactFiberUpdateQueue'; + createUpdate, + ReplaceState, + ForceUpdate, +} from './ReactUpdateQueue'; +import {NoWork} from './ReactFiberExpirationTime'; const fakeInternalInstance = {}; const isArray = Array.isArray; let didWarnAboutStateAssignmentForComponent; -let didWarnAboutUndefinedDerivedState; let didWarnAboutUninitializedState; let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; let didWarnAboutLegacyLifecyclesAndDerivedState; +let didWarnAboutUndefinedDerivedState; +let warnOnUndefinedDerivedState; let warnOnInvalidCallback; if (__DEV__) { didWarnAboutStateAssignmentForComponent = new Set(); - didWarnAboutUndefinedDerivedState = new Set(); didWarnAboutUninitializedState = new Set(); didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); + didWarnAboutUndefinedDerivedState = new Set(); const didWarnOnInvalidCallback = new Set(); @@ -71,6 +74,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, @@ -92,17 +110,43 @@ 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); + +export function applyDerivedStateFromProps( + workInProgress: Fiber, + getDerivedStateFromProps: (props: any, state: any) => any, + nextProps: any, +) { + const prevState = workInProgress.memoizedState; + + if (__DEV__) { + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromProps(nextProps, prevState); } } - return resultState; + + const partialState = getDerivedStateFromProps(nextProps, prevState); + + 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( @@ -120,64 +164,57 @@ export default function( hasContextChanged, } = legacyContext; - // Class component state updater - const updater = { + const classComponentUpdater = { isMounted, - enqueueSetState(instance, partialState, callback) { - const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); - } + enqueueSetState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createUpdate(expirationTime); + update.payload = payload; + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, - enqueueReplaceState(instance, state, callback) { - const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'replaceState'); - } + enqueueReplaceState(inst, payload, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createUpdate(expirationTime); + update.tag = ReplaceState; + update.payload = payload; + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'replaceState'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, - enqueueForceUpdate(instance, callback) { - const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'forceUpdate'); - } + enqueueForceUpdate(inst, callback) { + const fiber = ReactInstanceMap.get(inst); const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createUpdate(expirationTime); + update.tag = ForceUpdate; + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'forceUpdate'); + } + update.callback = callback; + } + + enqueueUpdate(fiber, update, expirationTime); scheduleWork(fiber, expirationTime); }, }; @@ -191,11 +228,10 @@ export default function( newContext, ) { if ( - oldProps === null || - (workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate) + workInProgress.updateQueue !== null && + workInProgress.updateQueue.hasForceUpdate ) { - // If the workInProgress already has an Update effect, return true + // If forceUpdate was called, disregard sCU. return true; } @@ -420,13 +456,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 +466,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); @@ -444,19 +479,21 @@ 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); - const state = + const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state - : null; + : null); adoptClassInstance(workInProgress, instance); if (__DEV__) { @@ -545,26 +582,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 +614,7 @@ export default function( getComponentName(workInProgress) || 'Component', ); } - updater.enqueueReplaceState(instance, instance.state, null); + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } } @@ -631,50 +648,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 +658,6 @@ export default function( renderExpirationTime: ExpirationTime, ): void { const ctor = workInProgress.type; - const current = workInProgress.alternate; if (__DEV__) { checkClassInstance(workInProgress); @@ -715,6 +688,29 @@ export default function( } } + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( + workInProgress, + updateQueue, + props, + instance, + renderExpirationTime, + ); + instance.state = workInProgress.memoizedState; + } + + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( + workInProgress, + getDerivedStateFromProps, + props, + ); + 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 +722,19 @@ 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, + processUpdateQueue( workInProgress, updateQueue, - instance, props, + instance, renderExpirationTime, ); + instance.state = workInProgress.memoizedState; } } + if (typeof instance.componentDidMount === 'function') { workInProgress.effectTag |= Update; } @@ -749,16 +746,18 @@ 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); + 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 @@ -782,93 +781,27 @@ 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, + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( workInProgress, - workInProgress.updateQueue, - instance, + updateQueue, newProps, + instance, 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; + newState = workInProgress.memoizedState; } - 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( + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( workInProgress, - instance, + getDerivedStateFromProps, newProps, - newState, ); - } - - 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 ( @@ -925,9 +858,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,16 +880,18 @@ 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); + 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 @@ -980,94 +915,27 @@ 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, + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue( workInProgress, - workInProgress.updateQueue, - instance, + updateQueue, newProps, + instance, 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; + newState = workInProgress.memoizedState; } - 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( + if (typeof getDerivedStateFromProps === 'function') { + applyDerivedStateFromProps( workInProgress, - instance, + getDerivedStateFromProps, newProps, - newState, ); - } - - 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 ( @@ -1154,8 +1022,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 +1037,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..a3754902975c1 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -33,15 +33,15 @@ import { ContentReset, Snapshot, } from 'shared/ReactTypeOfSideEffect'; +import {commitUpdateQueue} from './ReactUpdateQueue'; 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'; +import {logCapturedError} from './ReactFiberErrorLogger'; const { invokeGuardedCallback, @@ -54,7 +54,7 @@ if (__DEV__) { didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); } -function logError(boundary: Fiber, errorInfo: CapturedValue) { +export function logError(boundary: Fiber, errorInfo: CapturedValue) { const source = errorInfo.source; let stack = errorInfo.stack; if (stack === null) { @@ -251,7 +251,14 @@ export default function( } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitCallbacks(updateQueue, instance); + instance.props = finishedWork.memoizedProps; + instance.state = finishedWork.memoizedState; + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } return; } @@ -269,7 +276,12 @@ export default function( break; } } - commitCallbacks(updateQueue, instance); + commitUpdateQueue( + finishedWork, + updateQueue, + instance, + committedExpirationTime, + ); } return; } @@ -306,73 +318,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 +509,6 @@ export default function( }, commitLifeCycles, commitBeforeMutationLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, }; @@ -892,7 +836,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..519003f17719a 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -26,7 +26,7 @@ import warning from 'fbjs/lib/warning'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import ReactFiberScheduler from './ReactFiberScheduler'; -import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue'; +import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -339,28 +339,22 @@ export default function( } } + const update = createUpdate(expirationTime); + update.payload = {children: element}; + callback = callback === undefined ? null : callback; - if (__DEV__) { + if (callback !== null) { warning( - callback === null || typeof callback === 'function', + typeof callback === 'function', 'render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback, ); + update.callback = callback; } + enqueueUpdate(current, update, 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..1a9846b0bdbaf 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -31,7 +31,6 @@ import { Ref, Incomplete, HostEffectMask, - ErrLog, } from 'shared/ReactTypeOfSideEffect'; import { HostRoot, @@ -89,10 +88,7 @@ import { import {AsyncMode} from './ReactTypeOfMode'; import ReactFiberLegacyContext from './ReactFiberContext'; import ReactFiberNewContext from './ReactFiberNewContext'; -import { - getUpdateExpirationTime, - insertUpdateIntoFiber, -} from './ReactFiberUpdateQueue'; +import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; @@ -195,12 +191,16 @@ export default function( throwException, unwindWork, unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, } = ReactFiberUnwindWork( hostContext, legacyContext, newContext, scheduleWork, + markLegacyErrorBoundaryAsFailed, isAlreadyFailedLegacyErrorBoundary, + onUncaughtError, ); const { commitBeforeMutationLifeCycles, @@ -209,7 +209,6 @@ export default function( commitDeletion, commitWork, commitLifeCycles, - commitErrorLogging, commitAttachRef, commitDetachRef, } = ReactFiberCommitWork( @@ -447,10 +446,6 @@ export default function( ); } - if (effectTag & ErrLog) { - commitErrorLogging(nextEffect, onUncaughtError); - } - if (effectTag & Ref) { recordEffect(); commitAttachRef(nextEffect); @@ -681,7 +676,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 @@ -956,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); @@ -974,7 +984,12 @@ export default function( onUncaughtError(thrownValue); break; } - throwException(returnFiber, sourceFiber, thrownValue); + throwException( + returnFiber, + sourceFiber, + thrownValue, + nextRenderExpirationTime, + ); nextUnitOfWork = completeUnitOfWork(sourceFiber); } break; @@ -1022,22 +1037,6 @@ 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); - scheduleWork(boundaryFiber, expirationTime); - } - function dispatch( sourceFiber: Fiber, value: mixed, @@ -1048,8 +1047,6 @@ export default function( 'dispatch: Cannot dispatch during the render phase.', ); - // TODO: Handle arrays - let fiber = sourceFiber.return; while (fiber !== null) { switch (fiber.tag) { @@ -1061,14 +1058,28 @@ 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( + fiber, + errorInfo, + expirationTime, + ); + enqueueUpdate(fiber, update, expirationTime); + scheduleWork(fiber, expirationTime); return; + } } fiber = fiber.return; } @@ -1076,7 +1087,15 @@ 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( + 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 6565e4888df1b..e59324c3c4239 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -12,10 +12,16 @@ 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 type {CapturedValue} from './ReactCapturedValue'; +import type {Update} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {ensureUpdateQueues} from './ReactFiberUpdateQueue'; +import { + enqueueCapturedUpdate, + createUpdate, + CaptureUpdate, +} from './ReactUpdateQueue'; +import {logError} from './ReactFiberCommitWork'; import { ClassComponent, @@ -42,7 +48,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 { @@ -51,10 +59,71 @@ export default function( } = legacyContext; const {popProvider} = newContext; + function createRootErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, + ): Update { + const update = createUpdate(expirationTime); + // Unmount the root by rendering null. + update.tag = CaptureUpdate; + update.payload = {children: null}; + const error = errorInfo.value; + update.callback = () => { + onUncaughtError(error); + logError(fiber, errorInfo); + }; + return update; + } + + function createClassErrorUpdate( + fiber: Fiber, + errorInfo: CapturedValue, + expirationTime: ExpirationTime, + ): Update { + const update = createUpdate(expirationTime); + 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.callback = function callback() { + if ( + !enableGetDerivedStateFromCatch || + 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(this); + } + const error = errorInfo.value; + const stack = errorInfo.stack; + logError(fiber, errorInfo); + this.componentDidCatch(error, { + componentStack: stack !== null ? stack : '', + }); + }; + } + return update; + } + function throwException( returnFiber: Fiber, sourceFiber: Fiber, rawValue: mixed, + renderExpirationTime: ExpirationTime, ) { // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; @@ -67,18 +136,19 @@ export default function( do { switch (workInProgress.tag) { case HostRoot: { - // Uncaught error const errorInfo = value; - ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue< - any, - > = (workInProgress.updateQueue: any); - updateQueue.capturedValues = [errorInfo]; workInProgress.effectTag |= ShouldCapture; + const update = createRootErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } case ClassComponent: // Capture and retry + const errorInfo = value; const ctor = workInProgress.type; const instance = workInProgress.stateNode; if ( @@ -89,17 +159,14 @@ 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 = createClassErrorUpdate( + workInProgress, + errorInfo, + renderExpirationTime, + ); + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); return; } break; @@ -176,5 +243,7 @@ export default function( throwException, unwindWork, unwindInterruptedWork, + createRootErrorUpdate, + createClassErrorUpdate, }; } 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..574a3c07406d9 --- /dev/null +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -0,0 +1,640 @@ +/** + * 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 + */ + +// 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. + +import type {Fiber} from './ReactFiber'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +import {NoWork} from './ReactFiberExpirationTime'; +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, + + tag: 0 | 1 | 2 | 3, + payload: any, + callback: (() => mixed) | null, + + next: Update | null, + nextEffect: Update | null, +}; + +export type UpdateQueue = { + expirationTime: ExpirationTime, + baseState: State, + + firstUpdate: Update | null, + lastUpdate: Update | null, + + firstCapturedUpdate: Update | null, + lastCapturedUpdate: Update | null, + + firstEffect: Update | null, + lastEffect: Update | null, + + firstCapturedEffect: Update | null, + lastCapturedEffect: Update | null, + + // TODO: Workaround for lack of tuples. Could use global state instead. + hasForceUpdate: boolean, +}; + +export const UpdateState = 0; +export const ReplaceState = 1; +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 { + const queue: UpdateQueue = { + expirationTime: NoWork, + baseState, + firstUpdate: null, + lastUpdate: null, + firstCapturedUpdate: null, + lastCapturedUpdate: null, + firstEffect: null, + lastEffect: null, + firstCapturedEffect: null, + lastCapturedEffect: null, + hasForceUpdate: false, + }; + return queue; +} + +function cloneUpdateQueue( + currentQueue: UpdateQueue, +): UpdateQueue { + const queue: UpdateQueue = { + expirationTime: currentQueue.expirationTime, + baseState: currentQueue.baseState, + firstUpdate: currentQueue.firstUpdate, + lastUpdate: currentQueue.lastUpdate, + + // TODO: With resuming, if we bail out and resuse the child tree, we should + // keep these effects. + firstCapturedUpdate: null, + lastCapturedUpdate: null, + + hasForceUpdate: false, + + firstEffect: null, + lastEffect: null, + + firstCapturedEffect: null, + lastCapturedEffect: null, + }; + return queue; +} + +export function createUpdate(expirationTime: ExpirationTime): Update<*> { + return { + expirationTime: expirationTime, + + tag: UpdateState, + payload: null, + callback: null, + + next: null, + nextEffect: null, + }; +} + +function appendUpdateToQueue( + queue: UpdateQueue, + update: Update, + expirationTime: 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( + fiber: Fiber, + update: Update, + expirationTime: ExpirationTime, +) { + // Update queues are created lazily. + const alternate = fiber.alternate; + let queue1; + let queue2; + if (alternate === null) { + // There's only one fiber. + queue1 = fiber.updateQueue; + queue2 = null; + if (queue1 === null) { + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); + } + } else { + // There are two owners. + queue1 = fiber.updateQueue; + queue2 = alternate.updateQueue; + if (queue1 === null) { + if (queue2 === null) { + // Neither fiber has an update queue. Create new ones. + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); + queue2 = alternate.updateQueue = createUpdateQueue( + alternate.memoizedState, + ); + } else { + // Only one fiber has an update queue. Clone to create a new one. + queue1 = fiber.updateQueue = cloneUpdateQueue(queue2); + } + } else { + if (queue2 === null) { + // 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. + } + } + } + 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 ( + fiber.tag === ClassComponent && + (currentlyProcessingQueue === queue1 || + (queue2 !== null && currentlyProcessingQueue === queue2)) && + !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 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 ensureWorkInProgressQueueIsAClone( + workInProgress: Fiber, + 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 === current.updateQueue) { + queue = workInProgress.updateQueue = cloneUpdateQueue(queue); + } + } + 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 (__DEV__) { + 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 (__DEV__) { + 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 ( + queue.expirationTime === NoWork || + queue.expirationTime > renderExpirationTime + ) { + // Insufficient priority. Bailout. + return; + } + + queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); + + if (__DEV__) { + currentlyProcessingQueue = queue; + } + + // 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 = 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; + 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; + } + + // 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) { + 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 = 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; + if (queue.lastCapturedEffect === null) { + queue.firstCapturedEffect = queue.lastCapturedEffect = update; + } else { + queue.lastCapturedEffect.nextEffect = update; + queue.lastCapturedEffect = update; + } + } + } + update = update.next; + } + + if (newFirstUpdate === null) { + queue.lastUpdate = null; + } + if (newFirstCapturedUpdate === null) { + queue.lastCapturedUpdate = null; + } else { + workInProgress.effectTag |= Callback; + } + 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; + } + + queue.baseState = newBaseState; + queue.firstUpdate = newFirstUpdate; + queue.firstCapturedUpdate = newFirstCapturedUpdate; + queue.expirationTime = newExpirationTime; + + workInProgress.memoizedState = resultState; + + if (__DEV__) { + currentlyProcessingQueue = null; + } +} + +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 + // 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.firstCapturedUpdate !== null) { + // 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 captured updates. + finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null; + } + + // Commit the effects + let effect = finishedQueue.firstEffect; + finishedQueue.firstEffect = finishedQueue.lastEffect = null; + while (effect !== null) { + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); + } + effect = effect.nextEffect; + } + + effect = finishedQueue.firstCapturedEffect; + finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null; + while (effect !== null) { + const callback = effect.callback; + if (callback !== null) { + effect.callback = null; + callCallback(callback, instance); + } + effect = effect.nextEffect; + } +} diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index 91de8d285da29..a8b66373d8f48 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(); @@ -1421,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; @@ -1455,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', () => { 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/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); diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 82e8c3342fc70..27d6aa6090e45 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 Callback = /* */ 0b000000100000; -export const DidCapture = /* */ 0b000001000000; -export const Ref = /* */ 0b000010000000; -export const ErrLog = /* */ 0b000100000000; -export const Snapshot = /* */ 0b100000000000; +export const Placement = /* */ 0b00000000010; +export const Update = /* */ 0b00000000100; +export const PlacementAndUpdate = /* */ 0b00000000110; +export const Deletion = /* */ 0b00000001000; +export const ContentReset = /* */ 0b00000010000; +export const Callback = /* */ 0b00000100000; +export const DidCapture = /* */ 0b00001000000; +export const Ref = /* */ 0b00010000000; +export const Snapshot = /* */ 0b00100000000; // Union of all host effects -export const HostEffectMask = /* */ 0b100111111111; +export const HostEffectMask = /* */ 0b00111111111; -export const Incomplete = /* */ 0b001000000000; -export const ShouldCapture = /* */ 0b010000000000; +export const Incomplete = /* */ 0b01000000000; +export const ShouldCapture = /* */ 0b10000000000;