From b548b3cd640dbd515f5d67dafc0216bb7ee0d796 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 22 Apr 2018 23:05:28 -0700 Subject: [PATCH] Decouple update queue from Fiber type (#12600) * 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`. * 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. * 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. * 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. * 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. * 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). * 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. * 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. * Only double-invoke render phase lifecycles functions in DEV * Use global state to track currently processing queue in DEV --- .../createSubscription-test.internal.js | 14 +- packages/react-noop-renderer/src/ReactNoop.js | 16 +- packages/react-reconciler/src/ReactFiber.js | 2 +- .../src/ReactFiberBeginWork.js | 80 +-- .../src/ReactFiberClassComponent.js | 505 +++++--------- .../src/ReactFiberCommitWork.js | 91 +-- .../src/ReactFiberCompleteWork.js | 27 +- .../src/ReactFiberReconciler.js | 22 +- .../src/ReactFiberScheduler.js | 89 ++- .../src/ReactFiberUnwindWork.js | 105 ++- .../src/ReactFiberUpdateQueue.js | 394 ----------- .../react-reconciler/src/ReactUpdateQueue.js | 640 ++++++++++++++++++ .../ReactIncremental-test.internal.js | 11 +- .../ReactIncrementalTriangle-test.internal.js | 2 + ...ReactIncrementalPerf-test.internal.js.snap | 8 +- .../ReactStrictMode-test.internal.js | 82 ++- packages/shared/ReactTypeOfSideEffect.js | 29 +- 17 files changed, 1112 insertions(+), 1005 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberUpdateQueue.js create mode 100644 packages/react-reconciler/src/ReactUpdateQueue.js 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;