diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6de5e54877ec3a..d740e2867c370c 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -35,10 +35,12 @@ import { ContextConsumer, } from 'shared/ReactTypeOfWork'; import { + NoEffect, PerformedWork, Placement, ContentReset, Ref, + DidCapture, } from 'shared/ReactTypeOfSideEffect'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import { @@ -58,7 +60,12 @@ import { reconcileChildFibers, cloneChildFibers, } from './ReactChildFiber'; -import {processUpdateQueue} from './ReactFiberUpdateQueue'; +import {processUpdateQueue as processLegacyUpdateQueue} from './ReactFiberUpdateQueue'; +import { + createDeriveStateFromPropsUpdate, + enqueueRenderPhaseUpdate, + processUpdateQueue, +} from './ReactUpdateQueue'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncMode, StrictMode} from './ReactTypeOfMode'; import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; @@ -260,7 +267,11 @@ export default function( if (current === null) { if (workInProgress.stateNode === null) { // In the initial pass we might need to construct the instance. - constructClassInstance(workInProgress, workInProgress.pendingProps); + constructClassInstance( + workInProgress, + workInProgress.pendingProps, + renderExpirationTime, + ); mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; @@ -278,22 +289,11 @@ export default function( renderExpirationTime, ); } - - // We processed the update queue inside updateClassInstance. It may have - // included some errors that were dispatched during the commit phase. - // TODO: Refactor class components so this is less awkward. - let didCaptureError = false; - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.capturedValues !== null) { - shouldUpdate = true; - didCaptureError = true; - } return finishClassComponent( current, workInProgress, shouldUpdate, hasContext, - didCaptureError, renderExpirationTime, ); } @@ -303,12 +303,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) { @@ -414,7 +416,7 @@ export default function( let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { const prevState = workInProgress.memoizedState; - const state = processUpdateQueue( + const state = processLegacyUpdateQueue( current, workInProgress, updateQueue, @@ -607,20 +609,13 @@ export default function( workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null; - if (typeof Component.getDerivedStateFromProps === 'function') { - const partialState = callGetDerivedStateFromProps( - workInProgress, - value, - props, - workInProgress.memoizedState, - ); - - if (partialState !== null && partialState !== undefined) { - workInProgress.memoizedState = Object.assign( - {}, - workInProgress.memoizedState, - partialState, - ); + const getDerivedStateFromProps = Component.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createDeriveStateFromPropsUpdate(renderExpirationTime); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); + const updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); } } @@ -635,7 +630,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 3f811c11088a31..c4f77435ec4fdf 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -10,9 +10,8 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {LegacyContext} from './ReactFiberContext'; -import type {CapturedValue} from './ReactCapturedValue'; -import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect'; +import {Update, Snapshot, ForceUpdate} from 'shared/ReactTypeOfSideEffect'; import { enableGetDerivedStateFromCatch, debugRenderPhaseSideEffects, @@ -31,15 +30,20 @@ import warning from 'fbjs/lib/warning'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; import { - insertUpdateIntoFiber, + enqueueUpdate, + enqueueRenderPhaseUpdate, processUpdateQueue, -} from './ReactFiberUpdateQueue'; + createStateUpdate, + createStateReplace, + createCallbackEffect, + createDeriveStateFromPropsUpdate, + createForceUpdate, +} from './ReactUpdateQueue'; const fakeInternalInstance = {}; const isArray = Array.isArray; let didWarnAboutStateAssignmentForComponent; -let didWarnAboutUndefinedDerivedState; let didWarnAboutUninitializedState; let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; let didWarnAboutLegacyLifecyclesAndDerivedState; @@ -47,7 +51,6 @@ let warnOnInvalidCallback; if (__DEV__) { didWarnAboutStateAssignmentForComponent = new Set(); - didWarnAboutUndefinedDerivedState = new Set(); didWarnAboutUninitializedState = new Set(); didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); @@ -92,17 +95,16 @@ if (__DEV__) { }); Object.freeze(fakeInternalInstance); } -function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array) { - const resultState = {}; - for (let i = 0; i < capturedValues.length; i++) { - const capturedValue: CapturedValue = (capturedValues[i]: any); - const error = capturedValue.value; - const partialState = ctor.getDerivedStateFromCatch.call(null, error); - if (partialState !== null && partialState !== undefined) { - Object.assign(resultState, partialState); - } + +function enqueueDerivedStateFromProps( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): void { + const getDerivedStateFromProps = workInProgress.type.getDerivedStateFromProps; + if (typeof getDerivedStateFromProps === 'function') { + const update = createDeriveStateFromPropsUpdate(renderExpirationTime); + enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime); } - return resultState; } export default function( @@ -120,64 +122,57 @@ export default function( hasContextChanged, } = legacyContext; - // Class component state updater - const updater = { + const classComponentUpdater = { isMounted, - enqueueSetState(instance, partialState, callback) { + enqueueSetState(instance, payload, callback) { const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'setState'); - } const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createStateUpdate(payload, expirationTime); + enqueueUpdate(fiber, update, expirationTime); + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(fiber, callbackUpdate, expirationTime); + } + scheduleWork(fiber, expirationTime); }, - enqueueReplaceState(instance, state, callback) { + enqueueReplaceState(instance, payload, callback) { const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'replaceState'); - } const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createStateReplace(payload, expirationTime); + enqueueUpdate(fiber, update, expirationTime); + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(fiber, callbackUpdate, expirationTime); + } + scheduleWork(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); - callback = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(callback, 'forceUpdate'); - } const expirationTime = computeExpirationForFiber(fiber); - const update = { - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - capturedValue: null, - next: null, - }; - insertUpdateIntoFiber(fiber, update); + + const update = createForceUpdate(expirationTime); + enqueueUpdate(fiber, update, expirationTime); + + if (callback !== undefined && callback !== null) { + if (__DEV__) { + warnOnInvalidCallback(callback, 'setState'); + } + const callbackUpdate = createCallbackEffect(callback, expirationTime); + enqueueUpdate(fiber, callbackUpdate, expirationTime); + } + scheduleWork(fiber, expirationTime); }, }; @@ -190,11 +185,7 @@ export default function( newState, newContext, ) { - if ( - oldProps === null || - (workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate) - ) { + if (workInProgress.effectTag & ForceUpdate) { // If the workInProgress already has an Update effect, return true return true; } @@ -420,13 +411,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 +421,11 @@ export default function( } } - function constructClassInstance(workInProgress: Fiber, props: any): any { + function constructClassInstance( + workInProgress: Fiber, + props: any, + renderExpirationTime: ExpirationTime, + ): any { const ctor = workInProgress.type; const unmaskedContext = getUnmaskedContext(workInProgress); const needsContext = isContextConsumer(workInProgress); @@ -453,10 +443,10 @@ export default function( } const instance = new ctor(props, context); - const state = + const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state - : null; + : null); adoptClassInstance(workInProgress, instance); if (__DEV__) { @@ -545,26 +535,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 +567,7 @@ export default function( getComponentName(workInProgress) || 'Component', ); } - updater.enqueueReplaceState(instance, instance.state, null); + classComponentUpdater.enqueueReplaceState(instance, instance.state, null); } } @@ -631,50 +601,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 +611,6 @@ export default function( renderExpirationTime: ExpirationTime, ): void { const ctor = workInProgress.type; - const current = workInProgress.alternate; if (__DEV__) { checkClassInstance(workInProgress); @@ -715,6 +641,13 @@ export default function( } } + enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + instance.state = workInProgress.memoizedState; + } + // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( @@ -726,18 +659,13 @@ 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, - workInProgress, - updateQueue, - instance, - props, - renderExpirationTime, - ); + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + instance.state = workInProgress.memoizedState; } } + if (typeof instance.componentDidMount === 'function') { workInProgress.effectTag |= Update; } @@ -749,10 +677,11 @@ export default function( ): boolean { const ctor = workInProgress.type; const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); const oldProps = workInProgress.memoizedProps; const newProps = workInProgress.pendingProps; + instance.props = oldProps; + const oldContext = instance.context; const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); @@ -782,103 +711,24 @@ export default function( } } - // Compute the next state using the memoized state and the update queue. - const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - let derivedStateFromCatch; - if (workInProgress.updateQueue !== null) { - newState = processUpdateQueue( - null, - workInProgress, - workInProgress.updateQueue, - instance, - newProps, - renderExpirationTime, - ); - - let updateQueue = workInProgress.updateQueue; - if ( - updateQueue !== null && - updateQueue.capturedValues !== null && - (enableGetDerivedStateFromCatch && - typeof ctor.getDerivedStateFromCatch === 'function') - ) { - const capturedValues = updateQueue.capturedValues; - // Don't remove these from the update queue yet. We need them in - // finishClassComponent. Do the reset there. - // TODO: This is awkward. Refactor class components. - // updateQueue.capturedValues = null; - derivedStateFromCatch = callGetDerivedStateFromCatch( - ctor, - capturedValues, - ); - } - } else { - newState = oldState; - } - - let derivedStateFromProps; + // Only call getDerivedStateFromProps if the props have changed if (oldProps !== newProps) { - // The prevState parameter should be the partially updated state. - // Otherwise, spreading state in return values could override updates. - derivedStateFromProps = callGetDerivedStateFromProps( - workInProgress, - instance, - newProps, - newState, - ); + enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); } - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromProps - : Object.assign({}, newState, derivedStateFromProps); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromProps, - ); - } - } - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromCatch - : Object.assign({}, newState, derivedStateFromCatch); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromCatch, - ); - } + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + newState = workInProgress.memoizedState; } if ( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !(workInProgress.effectTag & ForceUpdate) ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. @@ -925,9 +775,9 @@ export default function( } // If shouldComponentUpdate returned false, we should still update the - // memoized props/state to indicate that this work can be reused. - memoizeProps(workInProgress, newProps); - memoizeState(workInProgress, newState); + // memoized state to indicate that this work can be reused. + workInProgress.memoizedProps = newProps; + workInProgress.memoizedState = newState; } // Update the existing instance's state, props, and context pointers even @@ -947,10 +797,11 @@ export default function( ): boolean { const ctor = workInProgress.type; const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); const oldProps = workInProgress.memoizedProps; const newProps = workInProgress.pendingProps; + instance.props = oldProps; + const oldContext = instance.context; const newUnmaskedContext = getUnmaskedContext(workInProgress); const newContext = getMaskedContext(workInProgress, newUnmaskedContext); @@ -980,104 +831,24 @@ export default function( } } - // Compute the next state using the memoized state and the update queue. - const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - let derivedStateFromCatch; - - if (workInProgress.updateQueue !== null) { - newState = processUpdateQueue( - current, - workInProgress, - workInProgress.updateQueue, - instance, - newProps, - renderExpirationTime, - ); - - let updateQueue = workInProgress.updateQueue; - if ( - updateQueue !== null && - updateQueue.capturedValues !== null && - (enableGetDerivedStateFromCatch && - typeof ctor.getDerivedStateFromCatch === 'function') - ) { - const capturedValues = updateQueue.capturedValues; - // Don't remove these from the update queue yet. We need them in - // finishClassComponent. Do the reset there. - // TODO: This is awkward. Refactor class components. - // updateQueue.capturedValues = null; - derivedStateFromCatch = callGetDerivedStateFromCatch( - ctor, - capturedValues, - ); - } - } else { - newState = oldState; - } - - let derivedStateFromProps; + // Only call getDerivedStateFromProps if the props have changed if (oldProps !== newProps) { - // The prevState parameter should be the partially updated state. - // Otherwise, spreading state in return values could override updates. - derivedStateFromProps = callGetDerivedStateFromProps( - workInProgress, - instance, - newProps, - newState, - ); + enqueueDerivedStateFromProps(workInProgress, renderExpirationTime); } - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromProps - : Object.assign({}, newState, derivedStateFromProps); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromProps, - ); - } - } - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) { - // Render-phase updates (like this) should not be added to the update queue, - // So that multiple render passes do not enqueue multiple updates. - // Instead, just synchronously merge the returned state into the instance. - newState = - newState === null || newState === undefined - ? derivedStateFromCatch - : Object.assign({}, newState, derivedStateFromCatch); - - // Update the base state of the update queue. - // FIXME: This is getting ridiculous. Refactor plz! - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - updateQueue.baseState = Object.assign( - {}, - updateQueue.baseState, - derivedStateFromCatch, - ); - } + const oldState = workInProgress.memoizedState; + let newState = (instance.state = oldState); + let updateQueue = workInProgress.updateQueue; + if (updateQueue !== null) { + processUpdateQueue(workInProgress, updateQueue, renderExpirationTime); + newState = workInProgress.memoizedState; } if ( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !(workInProgress.effectTag & ForceUpdate) ) { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. @@ -1154,8 +925,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 +940,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 afa5b46d0b0f8a..aa9d9690b0711c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -37,6 +37,7 @@ import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; import {commitCallbacks} from './ReactFiberUpdateQueue'; +import ReactUpdateQueue from './ReactUpdateQueue'; import {onCommitUnmount} from './ReactFiberDevToolsHook'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {logCapturedError} from './ReactFiberErrorLogger'; @@ -107,6 +108,11 @@ export default function( ) { const {getPublicInstance, mutation, persistence} = config; + const {commitUpdateQueue} = ReactUpdateQueue( + markLegacyErrorBoundaryAsFailed, + logError, + ); + const callComponentWillUnmountWithTimer = function(current, instance) { startPhaseTimer(current, 'componentWillUnmount'); instance.props = current.memoizedProps; @@ -251,7 +257,7 @@ export default function( } const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - commitCallbacks(updateQueue, instance); + commitUpdateQueue(finishedWork, updateQueue, committedExpirationTime); } return; } @@ -311,42 +317,6 @@ export default function( 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( diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 02779db8b65613..03aa3b25afd27a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -416,20 +416,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: { diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 47fd98f69b65da..0299876bcdd98c 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -27,7 +27,7 @@ import { Deletion, ContentReset, Callback, - DidCapture, + ShouldCapture, Ref, Incomplete, HostEffectMask, @@ -93,6 +93,7 @@ import { getUpdateExpirationTime, insertUpdateIntoFiber, } from './ReactFiberUpdateQueue'; +import {createCatchUpdate, enqueueUpdate} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import ReactFiberStack from './ReactFiberStack'; @@ -503,6 +504,8 @@ export default function( ); root.pendingCommitExpirationTime = NoWork; + // console.log('commitRoot', committedExpirationTime); + const currentTime = recalculateCurrentTime(); // Reset this to null before calling lifecycles @@ -799,7 +802,7 @@ export default function( // capture values if possible. const next = unwindWork(workInProgress); // Because this fiber did not complete, don't reset its expiration time. - if (workInProgress.effectTag & DidCapture) { + if (workInProgress.effectTag & ShouldCapture) { // Restarting an error boundary stopFailedWorkTimer(workInProgress); } else { @@ -859,6 +862,10 @@ export default function( // progress. const current = workInProgress.alternate; + // console.log( + // (workInProgress.type && workInProgress.type.name) || workInProgress.type, + // ); + // See if beginning this work spawns more work. startWorkTimer(workInProgress); if (__DEV__) { @@ -915,6 +922,7 @@ export default function( expirationTime: ExpirationTime, isAsync: boolean, ): Fiber | null { + // console.log('renderRoot', expirationTime); invariant( !isWorking, 'renderRoot was called recursively. This error is likely caused ' + @@ -974,7 +982,12 @@ export default function( onUncaughtError(thrownValue); break; } - throwException(returnFiber, sourceFiber, thrownValue); + throwException( + returnFiber, + sourceFiber, + thrownValue, + nextRenderExpirationTime, + ); nextUnitOfWork = completeUnitOfWork(sourceFiber); } break; @@ -1023,18 +1036,23 @@ 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); + // TODO + if (boundaryFiber.tag === HostRoot) { + const update = { + expirationTime, + partialState: null, + callback: null, + isReplace: false, + isForced: false, + capturedValue, + next: null, + }; + insertUpdateIntoFiber(boundaryFiber, update); + } else { + const update = createCatchUpdate(capturedValue, expirationTime); + enqueueUpdate(boundaryFiber, update, expirationTime); + } scheduleWork(boundaryFiber, expirationTime); } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 6565e4888df1b5..9950e44c022920 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -16,6 +16,7 @@ import type {UpdateQueue} from './ReactFiberUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import {ensureUpdateQueues} from './ReactFiberUpdateQueue'; +import {enqueueRenderPhaseUpdate, createCatchUpdate} from './ReactUpdateQueue'; import { ClassComponent, @@ -55,6 +56,7 @@ export default function( returnFiber: Fiber, sourceFiber: Fiber, rawValue: mixed, + renderExpirationTime: ExpirationTime, ) { // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; @@ -79,6 +81,7 @@ export default function( } case ClassComponent: // Capture and retry + const errorInfo = value; const ctor = workInProgress.type; const instance = workInProgress.stateNode; if ( @@ -89,17 +92,15 @@ export default function( typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance))) ) { - ensureUpdateQueues(workInProgress); - const updateQueue: UpdateQueue< - any, - > = (workInProgress.updateQueue: any); - const capturedValues = updateQueue.capturedValues; - if (capturedValues === null) { - updateQueue.capturedValues = [value]; - } else { - capturedValues.push(value); - } workInProgress.effectTag |= ShouldCapture; + + // Schedule the error boundary to re-render using updated state + const update = createCatchUpdate(errorInfo, renderExpirationTime); + enqueueRenderPhaseUpdate( + workInProgress, + update, + renderExpirationTime, + ); return; } break; @@ -116,7 +117,6 @@ export default function( popLegacyContextProvider(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { - workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; return workInProgress; } return null; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js new file mode 100644 index 00000000000000..c5877ac6992011 --- /dev/null +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -0,0 +1,868 @@ +/** + * 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. +// +// Render phase updates +// -------------------- +// +// A render phase update is one triggered during the render phase, while working +// on a work-in-progress tree. Our typical strategy of adding the update to both +// queues won't work, because if the work-in-progress is thrown out and +// restarted, we'll get duplicate updates. Instead, we only add render phase +// updates to the work-in-progress queue. +// +// Because normal updates are added to a persistent list that is shared between +// both queues, render phase updates go in a special list that only belongs to +// a single queue. This an artifact of structural sharing. If we instead +// implemented each queue as separate lists, we would append render phase +// updates to the end of the work-in-progress list. +// +// Examples of render phase updates: +// - getDerivedStateFromProps +// - getDerivedStateFromCatch +// - [future] loading state + +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +import { + debugRenderPhaseSideEffects, + debugRenderPhaseSideEffectsForStrictMode, +} from 'shared/ReactFeatureFlags'; +import {NoWork} from './ReactFiberExpirationTime'; +import { + Callback as FiberCallbackEffect, + ForceUpdate as ForceUpdateEffect, + ShouldCapture, + DidCapture, +} from 'shared/ReactTypeOfSideEffect'; +import {StrictMode} from './ReactTypeOfMode'; +import {ClassComponent} from 'shared/ReactTypeOfWork'; +import getComponentName from 'shared/getComponentName'; + +import invariant from 'fbjs/lib/invariant'; +import warning from 'fbjs/lib/warning'; + +export type TypeOfUpdate = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + +// An empty update. A no-op. Used for an effect update that already committed, +// to prevent it from firing multiple times. +const NoOp = 0; +// Used for updates that do not depend on the previous value. +const StateReplace = 1; +// Used for updates that do depend on the previous value. +const StateUpdate = 2; +// forceUpdate +const ForceUpdate = 3; +// getDerivedStateFromProps +const DeriveStateFromPropsUpdate = 4; +const DeriveStateFromError = 5; +const DeriveStateFromErrorAndErrorLogEffect = 6; +// Callbacks +const CallbackEffect = 7; + +export type TypeOfUpdateQueue = 0; +const ClassComponentUpdateQueue = 0; + +type UpdateShared = { + payload: Payload, + expirationTime: ExpirationTime, + next: U | null, + nextEffect: U | null, +}; + +type ClassUpdate = + // No-op + | ({tag: 0} & UpdateShared>) + // setState + | ({tag: 1} & UpdateShared< + $Shape | ((State, Props) => $Shape | null | void), + ClassUpdate, + >) + // replaceState + | ({tag: 2} & UpdateShared< + State | ((State, Props) => State | null | void), + ClassUpdate, + >) + // forceUpdate + | ({tag: 3} & UpdateShared>) + // getDerivedStateFromProps + | ({tag: 4} & UpdateShared>) + // getDerivedStateFromCatch + | ({tag: 5} & UpdateShared>) + // Error logging effect + | ({tag: 6} & UpdateShared>) + // Callback effect + | ({tag: 7} & UpdateShared>); + +type Update = ClassUpdate; + +type UpdateQueueShared = { + expirationTime: ExpirationTime, + baseState: S, + + firstUpdate: U | null, + lastUpdate: U | null, + + firstRenderPhaseUpdate: U | null, + lastRenderPhaseUpdate: U | null, + + firstEffect: U | null, + lastEffect: U | null, + + // DEV_only + isProcessing?: boolean, +}; + +type ClassUpdateQueue = {tag: 0} & UpdateQueueShared< + ClassUpdate, + State, +>; + +type UpdateQueue = ClassUpdateQueue; + +type UpdateQueueOwner = { + alternate: UpdateQueueOwner | null, + memoizedState: State, +}; + +let warnOnUndefinedDerivedState; +let didWarnUpdateInsideUpdate; +let didWarnAboutUndefinedDerivedState; +if (__DEV__) { + didWarnUpdateInsideUpdate = false; + didWarnAboutUndefinedDerivedState = new Set(); + + warnOnUndefinedDerivedState = function(workInProgress, partialState) { + if (partialState === undefined) { + const componentName = getComponentName(workInProgress) || 'Component'; + if (!didWarnAboutUndefinedDerivedState.has(componentName)) { + didWarnAboutUndefinedDerivedState.add(componentName); + warning( + false, + '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + + 'You have returned undefined.', + componentName, + ); + } + } + }; +} + +export function createUpdateQueue( + baseState: State, +): UpdateQueue { + const queue = { + tag: ClassComponentUpdateQueue, + expirationTime: NoWork, + baseState, + firstUpdate: null, + lastUpdate: null, + firstRenderPhaseUpdate: null, + lastRenderPhaseUpdate: null, + firstEffect: null, + lastEffect: null, + }; + if (__DEV__) { + queue.isProcessing = false; + } + return queue; +} + +export function cloneUpdateQueue( + currentQueue: UpdateQueue, +): UpdateQueue { + const queue = { + tag: currentQueue.tag, + expirationTime: currentQueue.expirationTime, + baseState: currentQueue.baseState, + firstUpdate: currentQueue.firstUpdate, + lastUpdate: currentQueue.lastUpdate, + + // These are only valid for the lifetime of a single work-in-progress. + firstRenderPhaseUpdate: null, + lastRenderPhaseUpdate: null, + firstEffect: null, + lastEffect: null, + }; + if (__DEV__) { + queue.isProcessing = false; + } + return queue; +} + +function createUpdate() { + return { + tag: NoOp, + payload: null, + expirationTime: NoWork, + next: null, + nextEffect: null, + }; +} + +export function createStateReplace( + payload: mixed, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(); + update.tag = StateReplace; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +export function createStateUpdate( + payload: mixed, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(); + update.tag = StateUpdate; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +export function createForceUpdate( + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(); + update.tag = ForceUpdate; + update.expirationTime = expirationTime; + return update; +} + +export function createDeriveStateFromPropsUpdate( + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(); + update.tag = DeriveStateFromPropsUpdate; + update.expirationTime = expirationTime; + return update; +} + +export function createCatchUpdate( + error: mixed, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(); + update.tag = DeriveStateFromErrorAndErrorLogEffect; + update.expirationTime = expirationTime; + update.payload = error; + return update; +} + +export function createCallbackEffect( + payload: mixed, + expirationTime: ExpirationTime, +): Update { + const update = createUpdate(); + update.tag = CallbackEffect; + update.expirationTime = expirationTime; + update.payload = payload; + return update; +} + +function appendUpdateToQueue( + queue: UpdateQueue, + update: Update, + expirationTime: ExpirationTime, +): void { + // Append the update to the end of the list. + if (queue.lastUpdate === null) { + // Queue is empty + queue.firstUpdate = queue.lastUpdate = update; + } else { + queue.lastUpdate.next = update; + queue.lastUpdate = update; + } + if ( + queue.expirationTime === NoWork || + queue.expirationTime > expirationTime + ) { + // The incoming update has the earliest expiration of any update in the + // queue. Update the queue's expiration time. + queue.expirationTime = expirationTime; + } +} + +export function enqueueUpdate( + owner: UpdateQueueOwner, + update: Update, + expirationTime: ExpirationTime, +) { + // Update queues are created lazily. + const alternate = owner.alternate; + let queue1; + let queue2; + if (alternate === null) { + // There's only one owner. + queue1 = owner.updateQueue; + queue2 = null; + if (queue1 === null) { + queue1 = owner.updateQueue = createUpdateQueue(owner.memoizedState); + } + } else { + // There are two owners. + queue1 = owner.updateQueue; + queue2 = alternate.updateQueue; + if (queue1 === null) { + if (queue2 === null) { + // Neither owner has an update queue. Create new ones. + queue1 = owner.updateQueue = createUpdateQueue(owner.memoizedState); + queue2 = alternate.updateQueue = createUpdateQueue( + alternate.memoizedState, + ); + } else { + // Only one owner has an update queue. Clone to create a new one. + queue1 = owner.updateQueue = cloneUpdateQueue(queue2); + } + } else { + if (queue2 === null) { + // Only one owner has an update queue. Clone to create a new one. + queue2 = alternate.updateQueue = cloneUpdateQueue(queue1); + } else { + // Both owners have an update queue. + } + } + } + if (queue2 === null || queue1 === queue2) { + // There's only a single queue. + appendUpdateToQueue(queue1, update, expirationTime); + } else { + // There are two queues. We need to append the update to both queues, + // while accounting for the persistent structure of the list — we don't + // want the same update to be added multiple times. + if (queue1.lastUpdate === null || queue2.lastUpdate === null) { + // One of the queues is not empty. We must add the update to both queues. + appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue2, update, expirationTime); + } else { + // Both queues are non-empty. The last update is the same in both lists, + // because of structural sharing. So, only append to one of the lists. + appendUpdateToQueue(queue1, update, expirationTime); + // But we still need to update the `lastUpdate` pointer of queue2. + queue2.lastUpdate = update; + } + } + + if (__DEV__) { + if ( + owner.tag === ClassComponent && + (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && + !didWarnUpdateInsideUpdate + ) { + warning( + false, + 'An update (setState, replaceState, or forceUpdate) was scheduled ' + + 'from inside an update function. Update functions should be pure, ' + + 'with zero side-effects. Consider using componentDidUpdate or a ' + + 'callback.', + ); + didWarnUpdateInsideUpdate = true; + } + } +} + +export function enqueueRenderPhaseUpdate( + workInProgressOwner: UpdateQueueOwner, State>, + update: Update, + renderExpirationTime: ExpirationTime, +) { + // Render phase updates go into a separate list, and only on the work-in- + // progress queue. + let workInProgressQueue = workInProgressOwner.updateQueue; + if (workInProgressQueue === null) { + workInProgressQueue = workInProgressOwner.updateQueue = createUpdateQueue( + workInProgressOwner.memoizedState, + ); + } else { + // TODO: I put this here rather than createWorkInProgress so that we don't + // clone the queue unnecessarily. There's surely a better way to + // structure this. + workInProgressQueue = ensureWorkInProgressQueueIsAClone( + workInProgressOwner, + workInProgressQueue, + ); + } + + // Append the update to the end of the list. + if (workInProgressQueue.lastRenderPhaseUpdate === null) { + // This is the first render phase update + workInProgressQueue.firstRenderPhaseUpdate = workInProgressQueue.lastRenderPhaseUpdate = update; + } else { + workInProgressQueue.lastRenderPhaseUpdate.next = update; + workInProgressQueue.lastRenderPhaseUpdate = update; + } + if ( + workInProgressQueue.expirationTime === NoWork || + workInProgressQueue.expirationTime > renderExpirationTime + ) { + // The incoming update has the earliest expiration of any update in the + // queue. Update the queue's expiration time. + workInProgressQueue.expirationTime = renderExpirationTime; + } +} + +function addToEffectList(queue, update) { + // Set this to null, in case it was mutated during an aborted render. + update.nextEffect = null; + if (queue.lastEffect === null) { + queue.firstEffect = queue.lastEffect = update; + } else { + queue.lastEffect.nextEffect = update; + queue.lastEffect = update; + } +} + +function processSingleClassUpdate(workInProgress, queue, update, prevState) { + const payload = update.payload; + switch (update.tag) { + case StateReplace: { + if (typeof payload === 'function') { + // Updater function + const instance = workInProgress.stateNode; + const nextProps = workInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the updater an extra time to help detect side-effects. + payload.call(instance, prevState, nextProps); + } + + return payload.call(instance, prevState, nextProps); + } + // State object + return payload; + } + case StateUpdate: { + let partialState; + if (typeof payload === 'function') { + // Updater function + const instance = workInProgress.stateNode; + const nextProps = workInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the updater an extra time to help detect side-effects. + payload.call(instance, prevState, nextProps); + } + + partialState = payload.call(instance, prevState, nextProps); + } else { + // Partial state object + partialState = payload; + } + if (partialState === null || partialState === undefined) { + // Null and undefined are treated as no-ops. + return prevState; + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } + case ForceUpdate: { + workInProgress.effectTag |= ForceUpdateEffect; + return prevState; + } + case DeriveStateFromPropsUpdate: { + const getDerivedStateFromProps = + workInProgress.type.getDerivedStateFromProps; + const nextProps = workInProgress.pendingProps; + + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromProps(nextProps, prevState); + } + + const partialState = getDerivedStateFromProps(nextProps, prevState); + + if (__DEV__) { + warnOnUndefinedDerivedState(workInProgress, partialState); + } + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } + case DeriveStateFromErrorAndErrorLogEffect: { + const instance = workInProgress.stateNode; + if (typeof instance.componentDidCatch === 'function') { + workInProgress.effectTag |= FiberCallbackEffect; + addToEffectList(queue, update); + } + } + // Intentional fall-through to the next case, to calculate the derived state + // eslint-disable-next-line no-fallthrough + case DeriveStateFromError: { + const errorInfo = update.payload; + const getDerivedStateFromCatch = + workInProgress.type.getDerivedStateFromCatch; + + workInProgress.effectTag = + (workInProgress.effectTag & ~ShouldCapture) | DidCapture; + + if (typeof getDerivedStateFromCatch === 'function') { + const error = errorInfo.value; + if ( + debugRenderPhaseSideEffects || + (debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode) + ) { + // Invoke the function an extra time to help detect side-effects. + getDerivedStateFromCatch(error); + } + + // TODO: Pass prevState as second argument? + const partialState = getDerivedStateFromCatch(error); + + // Merge the partial state and the previous state. + return Object.assign({}, prevState, partialState); + } else { + return prevState; + } + } + case CallbackEffect: { + workInProgress.effectTag |= FiberCallbackEffect; + addToEffectList(queue, update); + return prevState; + } + case ErrorLogEffect: { + workInProgress.effectTag |= FiberCallbackEffect; + addToEffectList(queue, update); + return prevState; + } + default: + return prevState; + } +} + +function processSingleUpdate(owner, queue, update, prevState) { + switch (queue.tag) { + case ClassComponentUpdateQueue: + return processSingleClassUpdate(owner, queue, update, prevState); + default: + return prevState; + } +} + +function ensureWorkInProgressQueueIsAClone(owner, queue) { + const alternate = owner.alternate; + if (alternate !== null) { + // If the work-in-progress queue is equal to the current queue, + // we need to clone it first. + if (queue === alternate.updateQueue) { + queue = owner.updateQueue = cloneUpdateQueue(queue); + } + } + return queue; +} + +export function processUpdateQueue( + owner: UpdateQueueOwner, State>, + queue: UpdateQueue, + renderExpirationTime: ExpirationTime, +): void { + if ( + queue.expirationTime === NoWork || + queue.expirationTime > renderExpirationTime + ) { + // Insufficient priority. Bailout. + return; + } + + queue = ensureWorkInProgressQueueIsAClone(owner, queue); + + if (__DEV__) { + queue.isProcessing = true; + } + + // These values may change as we process the queue. + let newBaseState = queue.baseState; + let newFirstUpdate = null; + let newExpirationTime = NoWork; + + // Iterate through the list of updates to compute the result. + let update = queue.firstUpdate; + let resultState = newBaseState; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstUpdate === null) { + // This is the first skipped update. It will be the first update in + // the new list. + newFirstUpdate = update; + // Since this is the first update that was skipped, the current result + // is the new base state. + newBaseState = resultState; + } + // Since this update will remain in the list, update the remaining + // expiration time. + if ( + newExpirationTime === NoWork || + newExpirationTime > updateExpirationTime + ) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. Process it and compute + // a new result. + resultState = processSingleUpdate(owner, queue, update, resultState); + } + // Continue to the next update. + update = update.next; + } + + // Separately, iterate though the list of render phase updates. + let newFirstRenderPhaseUpdate = null; + update = queue.firstRenderPhaseUpdate; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + if (newFirstRenderPhaseUpdate === null) { + // This is the first skipped render phase update. It will be the first + // update in the new list. + newFirstUpdate = update; + // If this is the first update that was skipped (including the non- + // render phase updates!), the current result is the new base state. + if (newFirstUpdate === null) { + newBaseState = resultState; + } + } + // Since this update will remain in the list, update the remaining + // expiration time. + if ( + newExpirationTime === NoWork || + newExpirationTime > updateExpirationTime + ) { + newExpirationTime = updateExpirationTime; + } + } else { + // This update does have sufficient priority. Process it and compute + // a new result. + resultState = processSingleUpdate(owner, queue, update, resultState); + } + update = update.next; + } + if (newFirstUpdate === null) { + queue.lastUpdate = null; + } + if (newFirstRenderPhaseUpdate === null) { + queue.lastRenderPhaseUpdate = null; + } + if (newFirstUpdate === null && newFirstRenderPhaseUpdate === null) { + // We processed every update, without skipping. That means the new base + // state is the same as the result state. + newBaseState = resultState; + } + + queue.baseState = newBaseState; + queue.firstUpdate = newFirstUpdate; + queue.firstRenderPhaseUpdate = newFirstRenderPhaseUpdate; + queue.expirationTime = newExpirationTime; + + owner.memoizedState = resultState; + + if (__DEV__) { + queue.isProcessing = false; + } +} + +export default function( + markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, + // TODO: Move logError to this module + logError, +) { + function commitClassEffect(finishedWork, effect) { + const payload = effect.payload; + switch (effect.tag) { + case CallbackEffect: { + // Change the effect to no-op so it doesn't fire more than once. + effect.tag = NoOp; + effect.payload = null; + + const instance = finishedWork.stateNode; + const callback = payload; + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(instance); + break; + } + case DeriveStateFromErrorAndErrorLogEffect: { + // Change the tag to DeriveStateFromError so that we derive state + // correctly on rebase, but we don't log more than once. + effect.tag = DeriveStateFromError; + + const errorInfo = effect.payload; + const instance = finishedWork.stateNode; + const ctor = finishedWork.type; + + 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; + const error = errorInfo.value; + const stack = errorInfo.stack; + logError(finishedWork, errorInfo); + instance.componentDidCatch(error, { + componentStack: stack !== null ? stack : '', + }); + break; + } + } + } + + function commitEffect(owner, queue, effect) { + switch (queue.tag) { + case ClassComponentUpdateQueue: { + commitClassEffect(owner, effect); + break; + } + } + } + + function commitUpdateQueue( + owner: any, + finishedQueue: UpdateQueue, + renderExpirationTime: ExpirationTime, + ): void { + // If the finished render included render phase updates, and there are still + // lower priority updates left over, we need to keep the render phase updates + // in the queue so that they are rebased and not dropped once we process the + // queue again at the lower priority. + if (finishedQueue.firstRenderPhaseUpdate !== null) { + // Join the render phase update list to the end of the normal list. + if (finishedQueue.lastUpdate === null) { + // This should be unreachable. + if (__DEV__) { + warning(false, 'Expected a non-empty queue.'); + } + } else { + finishedQueue.lastUpdate.next = finishedQueue.firstRenderPhaseUpdate; + finishedQueue.lastUpdate = finishedQueue.lastRenderPhaseUpdate; + } + if ( + finishedQueue.expirationTime === NoWork || + finishedQueue.expirationTime > renderExpirationTime + ) { + // Update the queue's expiration time. + finishedQueue.expirationTime = renderExpirationTime; + } + // Clear the list of render phase updates. + finishedQueue.firstRenderPhaseUpdate = finishedQueue.lastRenderPhaseUpdate = null; + } + + // Commit the effects + let effect = finishedQueue.firstEffect; + finishedQueue.firstEffect = finishedQueue.lastEffect = null; + while (effect !== null) { + commitEffect(owner, finishedQueue, effect); + effect = effect.nextEffect; + } + } + + return { + commitUpdateQueue, + }; +} diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index 91de8d285da291..e9f22dec8c16fc 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1003,6 +1003,7 @@ describe('ReactIncremental', () => { instance.setState(updater); ReactNoop.flush(); expect(instance.state.num).toEqual(2); + instance.setState(updater); ReactNoop.render(); ReactNoop.flush(); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js index 628e1c38806b56..731865da65d7c8 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 48d0e6f4f67cd2..103baf8949a39f 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 @@ -248,7 +248,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] diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 82e8c3342fc702..cc2873e056fbc5 100644 --- a/packages/shared/ReactTypeOfSideEffect.js +++ b/packages/shared/ReactTypeOfSideEffect.js @@ -10,23 +10,24 @@ 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 = /* */ 0b0000000000000; +export const PerformedWork = /* */ 0b0000000000001; // 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 = /* */ 0b0000000000010; +export const Update = /* */ 0b0000000000100; +export const PlacementAndUpdate = /* */ 0b0000000000110; +export const Deletion = /* */ 0b0000000001000; +export const ContentReset = /* */ 0b0000000010000; +export const Callback = /* */ 0b0000000100000; +export const DidCapture = /* */ 0b0000001000000; +export const Ref = /* */ 0b0000010000000; +export const ErrLog = /* */ 0b0000100000000; +export const Snapshot = /* */ 0b0001000000000; +export const ForceUpdate = /* */ 0b0010000000000; // Union of all host effects -export const HostEffectMask = /* */ 0b100111111111; +export const HostEffectMask = /* */ 0b0011111111111; -export const Incomplete = /* */ 0b001000000000; -export const ShouldCapture = /* */ 0b010000000000; +export const Incomplete = /* */ 0b0100000000000; +export const ShouldCapture = /* */ 0b1000000000000;