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
This commit is contained in:
Andrew Clark
2018-04-22 23:05:28 -07:00
committed by GitHub
parent 5dcf93d146
commit b548b3cd64
17 changed files with 1111 additions and 1004 deletions

View File

@@ -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 (
<Subscription source={this.state.observed}>
{(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(<Parent observed={observableA} />);
// 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 (
<Subscription source={this.state.observed}>
{(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(<Parent observed={observableA} />);
// Flush everything and ensure that the correct subscribable is used
// We expect the new subscribable to finish rendering,

View File

@@ -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<mixed>, 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 + ']',
);
}

View File

@@ -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';

View File

@@ -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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const {
adoptClassInstance,
callGetDerivedStateFromProps,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
@@ -263,7 +266,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
ReactDebugCurrentFiber.setCurrentPhase(null);
} else {
if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode)
) {
instance.render();
}
nextChildren = instance.render();
}
}
@@ -416,29 +407,24 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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;
const nextState = workInProgress.memoizedState;
const nextChildren = nextState.children;
let element;
if (updateQueue !== null && updateQueue.capturedValues !== null) {
// There's an uncaught error. Unmount the whole root.
element = null;
} else if (prevState === state) {
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
workInProgress,
true,
hasContext,
false,
renderExpirationTime,
);
} else {

View File

@@ -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<mixed>) {
const resultState = {};
for (let i = 0; i < capturedValues.length; i++) {
const capturedValue: CapturedValue<mixed> = (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,

View File

@@ -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<mixed>) {
export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
const source = errorInfo.source;
let stack = errorInfo.stack;
if (stack === null) {
@@ -251,7 +251,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
break;
}
}
commitCallbacks(updateQueue, instance);
commitUpdateQueue(
finishedWork,
updateQueue,
instance,
committedExpirationTime,
);
}
return;
}
@@ -306,73 +318,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
},
commitLifeCycles,
commitBeforeMutationLifeCycles,
commitErrorLogging,
commitAttachRef,
commitDetachRef,
};
@@ -892,7 +836,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
commitDeletion,
commitWork,
commitLifeCycles,
commitErrorLogging,
commitAttachRef,
commitDetachRef,
};

View File

@@ -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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
workInProgress.effectTag &= ~Placement;
}
updateHostContainer(workInProgress);
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null && updateQueue.capturedValues !== null) {
workInProgress.effectTag |= ErrLog;
}
return null;
}
case HostComponent: {

View File

@@ -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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
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;
}

View File

@@ -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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
throwException,
unwindWork,
unwindInterruptedWork,
createRootErrorUpdate,
createClassErrorUpdate,
} = ReactFiberUnwindWork(
hostContext,
legacyContext,
newContext,
scheduleWork,
markLegacyErrorBoundaryAsFailed,
isAlreadyFailedLegacyErrorBoundary,
onUncaughtError,
);
const {
commitBeforeMutationLifeCycles,
@@ -209,7 +209,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
commitDeletion,
commitWork,
commitLifeCycles,
commitErrorLogging,
commitAttachRef,
commitDetachRef,
} = ReactFiberCommitWork(
@@ -447,10 +446,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
);
}
if (effectTag & ErrLog) {
commitErrorLogging(nextEffect, onUncaughtError);
}
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
@@ -681,7 +676,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
// 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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
onUncaughtError(thrownValue);
break;
}
throwException(returnFiber, sourceFiber, thrownValue);
throwException(
returnFiber,
sourceFiber,
thrownValue,
nextRenderExpirationTime,
);
nextUnitOfWork = completeUnitOfWork(sourceFiber);
}
break;
@@ -1022,22 +1037,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
'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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
(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<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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);
}
}

View File

@@ -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<C, CX>(
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<C, CX>(
} = legacyContext;
const {popProvider} = newContext;
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<null> {
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<mixed>,
expirationTime: ExpirationTime,
): Update<mixed> {
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<C, CX>(
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<C, CX>(
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<C, CX>(
throwException,
unwindWork,
unwindInterruptedWork,
createRootErrorUpdate,
createClassErrorUpdate,
};
}

View File

@@ -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<State, Props> =
| $Subtype<State>
| ((prevState: State, props: Props) => $Subtype<State>);
// Callbacks are not validated until invocation
type Callback = mixed;
export type Update<State> = {
expirationTime: ExpirationTime,
partialState: PartialState<any, any>,
callback: Callback | null,
isReplace: boolean,
isForced: boolean,
capturedValue: CapturedValue<mixed> | null,
next: Update<State> | 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<State> = {
// 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<State> | null,
last: Update<State> | null,
callbackList: Array<Update<State>> | null,
hasForceUpdate: boolean,
isInitialized: boolean,
capturedValues: Array<CapturedValue<mixed>> | null,
// Dev only
isProcessing?: boolean,
};
function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
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<State>(
queue: UpdateQueue<State>,
update: Update<State>,
): 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<State>(
fiber: Fiber,
update: Update<State>,
): 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<State>(
current: Fiber | null,
workInProgress: Fiber,
queue: UpdateQueue<State>,
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<State>(
queue: UpdateQueue<State>,
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);
}
}

View File

@@ -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<State> = {
expirationTime: ExpirationTime,
tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
expirationTime: ExpirationTime,
baseState: State,
firstUpdate: Update<State> | null,
lastUpdate: Update<State> | null,
firstCapturedUpdate: Update<State> | null,
lastCapturedUpdate: Update<State> | null,
firstEffect: Update<State> | null,
lastEffect: Update<State> | null,
firstCapturedEffect: Update<State> | null,
lastCapturedEffect: Update<State> | 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<State>(baseState: State): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
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<State>(
currentQueue: UpdateQueue<State>,
): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
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<State>(
queue: UpdateQueue<State>,
update: Update<State>,
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<State>(
fiber: Fiber,
update: Update<State>,
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<State>(
workInProgress: Fiber,
update: Update<State>,
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<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
): UpdateQueue<State> {
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<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
update: Update<State>,
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<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
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<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
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;
}
}

View File

@@ -1003,6 +1003,7 @@ describe('ReactIncremental', () => {
instance.setState(updater);
ReactNoop.flush();
expect(instance.state.num).toEqual(2);
instance.setState(updater);
ReactNoop.render(<Foo multiplier={3} />);
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', () => {

View File

@@ -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);

View File

@@ -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)
"
`;

View File

@@ -57,38 +57,64 @@ describe('ReactStrictMode', () => {
const component = ReactTestRenderer.create(<ClassComponent />);
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(<ClassComponent />);
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(<ClassComponent />);
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(<Root />);
if (debugRenderPhaseSideEffectsForStrictMode) {
if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) {
expect(log).toEqual([
'constructor',
'constructor',
@@ -197,7 +223,7 @@ describe('ReactStrictMode', () => {
shouldComponentUpdate = true;
component.update(<Root />);
if (debugRenderPhaseSideEffectsForStrictMode) {
if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) {
expect(log).toEqual([
'getDerivedStateFromProps',
'getDerivedStateFromProps',
@@ -219,7 +245,7 @@ describe('ReactStrictMode', () => {
shouldComponentUpdate = false;
component.update(<Root />);
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);

View File

@@ -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;