@samitouri / QOS-React / commits / b548b3cd64

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

Andrew Clark committed Apr 22, 2018 at 23:05 UTC b548b3cd640dbd515f5d67dafc0216bb7ee0d796
17 files changed +1112 -1005
packages/create-subscription/src/__tests__/createSubscription-test.internal.js
+4 -10
@@ -264,7 +264,6 @@ describe('createSubscription', () => {
264
265 it('should ignore values emitted by a new subscribable until the commit phase', () => {
266 const log = [];
267 - let parentInstance;
267
268 function Child({value}) {
269 ReactNoop.yield('Child: ' + value);
@@ -301,8 +300,6 @@ describe('createSubscription', () => {
300 }
301
302 render() {
304 - parentInstance = this;
305 -
303 return (
304 <Subscription source={this.state.observed}>
305 {(value = 'default') => {
@@ -331,8 +328,8 @@ describe('createSubscription', () => {
328 observableB.next('b-2');
329 observableB.next('b-3');
330
334 - // Mimic a higher-priority interruption
335 - parentInstance.setState({observed: observableA});
331 + // Update again
332 + ReactNoop.render(<Parent observed={observableA} />);
333
334 // Flush everything and ensure that the correct subscribable is used
335 // We expect the last emitted update to be rendered (because of the commit phase value check)
@@ -354,7 +351,6 @@ describe('createSubscription', () => {
351
352 it('should not drop values emitted between updates', () => {
353 const log = [];
357 - let parentInstance;
354
355 function Child({value}) {
356 ReactNoop.yield('Child: ' + value);
@@ -391,8 +387,6 @@ describe('createSubscription', () => {
387 }
388
389 render() {
394 - parentInstance = this;
395 -
390 return (
391 <Subscription source={this.state.observed}>
392 {(value = 'default') => {
@@ -420,8 +414,8 @@ describe('createSubscription', () => {
414 observableA.next('a-1');
415 observableA.next('a-2');
416
423 - // Mimic a higher-priority interruption
424 - parentInstance.setState({observed: observableA});
417 + // Update again
418 + ReactNoop.render(<Parent observed={observableA} />);
419
420 // Flush everything and ensure that the correct subscribable is used
421 // We expect the new subscribable to finish rendering,
packages/react-noop-renderer/src/ReactNoop.js
+4 -12
@@ -15,7 +15,7 @@
15 */
16
17 import type {Fiber} from 'react-reconciler/src/ReactFiber';
18 -import type {UpdateQueue} from 'react-reconciler/src/ReactFiberUpdateQueue';
18 +import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
19 import type {ReactNodeList} from 'shared/ReactTypes';
20 import ReactFiberReconciler from 'react-reconciler';
21 import {enablePersistentReconciler} from 'shared/ReactFeatureFlags';
@@ -526,23 +526,15 @@ const ReactNoop = {
526
527 function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
528 log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
529 - const firstUpdate = updateQueue.first;
529 + const firstUpdate = updateQueue.firstUpdate;
530 if (!firstUpdate) {
531 return;
532 }
533
534 - log(
535 - ' '.repeat(depth + 1) + '~',
536 - firstUpdate && firstUpdate.partialState,
537 - firstUpdate.callback ? 'with callback' : '',
538 - '[' + firstUpdate.expirationTime + ']',
539 - );
540 - let next;
541 - while ((next = firstUpdate.next)) {
534 + log(' '.repeat(depth + 1) + '~', '[' + firstUpdate.expirationTime + ']');
535 + while (firstUpdate.next) {
536 log(
537 ' '.repeat(depth + 1) + '~',
544 - next.partialState,
545 - next.callback ? 'with callback' : '',
538 '[' + firstUpdate.expirationTime + ']',
539 );
540 }
packages/react-reconciler/src/ReactFiber.js
+1 -1
@@ -12,7 +12,7 @@ import type {TypeOfWork} from 'shared/ReactTypeOfWork';
12 import type {TypeOfMode} from './ReactTypeOfMode';
13 import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect';
14 import type {ExpirationTime} from './ReactFiberExpirationTime';
15 -import type {UpdateQueue} from './ReactFiberUpdateQueue';
15 +import type {UpdateQueue} from './ReactUpdateQueue';
16
17 import invariant from 'fbjs/lib/invariant';
18 import {NoEffect} from 'shared/ReactTypeOfSideEffect';
packages/react-reconciler/src/ReactFiberBeginWork.js
+28 -52
@@ -36,10 +36,12 @@ import {
36 ContextConsumer,
37 } from 'shared/ReactTypeOfWork';
38 import {
39 + NoEffect,
40 PerformedWork,
41 Placement,
42 ContentReset,
43 Ref,
44 + DidCapture,
45 } from 'shared/ReactTypeOfSideEffect';
46 import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
47 import {
@@ -53,13 +55,15 @@ import warning from 'fbjs/lib/warning';
55 import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
56 import {cancelWorkTimer} from './ReactDebugFiberPerf';
57
56 -import ReactFiberClassComponent from './ReactFiberClassComponent';
58 +import ReactFiberClassComponent, {
59 + applyDerivedStateFromProps,
60 +} from './ReactFiberClassComponent';
61 import {
62 mountChildFibers,
63 reconcileChildFibers,
64 cloneChildFibers,
65 } from './ReactChildFiber';
62 -import {processUpdateQueue} from './ReactFiberUpdateQueue';
66 +import {processUpdateQueue} from './ReactUpdateQueue';
67 import {NoWork, Never} from './ReactFiberExpirationTime';
68 import {AsyncMode, StrictMode} from './ReactTypeOfMode';
69 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>(
112
113 const {
114 adoptClassInstance,
111 - callGetDerivedStateFromProps,
115 constructClassInstance,
116 mountClassInstance,
117 resumeMountClassInstance,
@@ -263,7 +266,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
266 if (current === null) {
267 if (workInProgress.stateNode === null) {
268 // In the initial pass we might need to construct the instance.
266 - constructClassInstance(workInProgress, workInProgress.pendingProps);
269 + constructClassInstance(
270 + workInProgress,
271 + workInProgress.pendingProps,
272 + renderExpirationTime,
273 + );
274 mountClassInstance(workInProgress, renderExpirationTime);
275
276 shouldUpdate = true;
@@ -281,22 +288,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
288 renderExpirationTime,
289 );
290 }
284 -
285 - // We processed the update queue inside updateClassInstance. It may have
286 - // included some errors that were dispatched during the commit phase.
287 - // TODO: Refactor class components so this is less awkward.
288 - let didCaptureError = false;
289 - const updateQueue = workInProgress.updateQueue;
290 - if (updateQueue !== null && updateQueue.capturedValues !== null) {
291 - shouldUpdate = true;
292 - didCaptureError = true;
293 - }
291 return finishClassComponent(
292 current,
293 workInProgress,
294 shouldUpdate,
295 hasContext,
299 - didCaptureError,
296 renderExpirationTime,
297 );
298 }
@@ -306,12 +302,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
302 workInProgress: Fiber,
303 shouldUpdate: boolean,
304 hasContext: boolean,
309 - didCaptureError: boolean,
305 renderExpirationTime: ExpirationTime,
306 ) {
307 // Refs should update even if shouldComponentUpdate returns false
308 markRef(current, workInProgress);
309
310 + const didCaptureError =
311 + (workInProgress.effectTag & DidCapture) !== NoEffect;
312 +
313 if (!shouldUpdate && !didCaptureError) {
314 // Context providers should defer to sCU for rendering
315 if (hasContext) {
@@ -351,13 +349,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
349 }
350 ReactDebugCurrentFiber.setCurrentPhase(null);
351 } else {
354 - if (
355 - debugRenderPhaseSideEffects ||
356 - (debugRenderPhaseSideEffectsForStrictMode &&
357 - workInProgress.mode & StrictMode)
358 - ) {
359 - instance.render();
360 - }
352 nextChildren = instance.render();
353 }
354 }
@@ -416,29 +407,24 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
407 pushHostRootContext(workInProgress);
408 let updateQueue = workInProgress.updateQueue;
409 if (updateQueue !== null) {
410 + const nextProps = workInProgress.pendingProps;
411 const prevState = workInProgress.memoizedState;
420 - const state = processUpdateQueue(
421 - current,
412 + const prevChildren = prevState !== null ? prevState.children : null;
413 + processUpdateQueue(
414 workInProgress,
415 updateQueue,
424 - null,
416 + nextProps,
417 null,
418 renderExpirationTime,
419 );
428 - memoizeState(workInProgress, state);
429 - updateQueue = workInProgress.updateQueue;
430 -
431 - let element;
432 - if (updateQueue !== null && updateQueue.capturedValues !== null) {
433 - // There's an uncaught error. Unmount the whole root.
434 - element = null;
435 - } else if (prevState === state) {
420 + const nextState = workInProgress.memoizedState;
421 + const nextChildren = nextState.children;
422 +
423 + if (nextChildren === prevChildren) {
424 // If the state is the same as before, that's a bailout because we had
425 // no work that expires at this time.
426 resetHydrationState();
427 return bailoutOnAlreadyFinishedWork(current, workInProgress);
440 - } else {
441 - element = state.element;
428 }
429 const root: FiberRoot = workInProgress.stateNode;
430 if (
@@ -463,16 +449,15 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
449 workInProgress.child = mountChildFibers(
450 workInProgress,
451 null,
466 - element,
452 + nextChildren,
453 renderExpirationTime,
454 );
455 } else {
456 // Otherwise reset hydration state in case we aborted and resumed another
457 // root.
458 resetHydrationState();
473 - reconcileChildren(current, workInProgress, element);
459 + reconcileChildren(current, workInProgress, nextChildren);
460 }
475 - memoizeState(workInProgress, state);
461 return workInProgress.child;
462 }
463 resetHydrationState();
@@ -610,21 +595,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
595 workInProgress.memoizedState =
596 value.state !== null && value.state !== undefined ? value.state : null;
597
613 - if (typeof Component.getDerivedStateFromProps === 'function') {
614 - const partialState = callGetDerivedStateFromProps(
598 + const getDerivedStateFromProps = Component.getDerivedStateFromProps;
599 + if (typeof getDerivedStateFromProps === 'function') {
600 + applyDerivedStateFromProps(
601 workInProgress,
616 - value,
602 + getDerivedStateFromProps,
603 props,
618 - workInProgress.memoizedState,
604 );
620 -
621 - if (partialState !== null && partialState !== undefined) {
622 - workInProgress.memoizedState = Object.assign(
623 - {},
624 - workInProgress.memoizedState,
625 - partialState,
626 - );
627 - }
605 }
606
607 // 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>(
615 workInProgress,
616 true,
617 hasContext,
641 - false,
618 renderExpirationTime,
619 );
620 } else {
packages/react-reconciler/src/ReactFiberClassComponent.js
+186 -319
@@ -10,11 +10,9 @@
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 import type {LegacyContext} from './ReactFiberContext';
13 -import type {CapturedValue} from './ReactCapturedValue';
13
14 import {Update, Snapshot} from 'shared/ReactTypeOfSideEffect';
15 import {
17 - enableGetDerivedStateFromCatch,
16 debugRenderPhaseSideEffects,
17 debugRenderPhaseSideEffectsForStrictMode,
18 warnAboutDeprecatedLifecycles,
@@ -31,26 +29,31 @@ import warning from 'fbjs/lib/warning';
29 import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
30 import {StrictMode} from './ReactTypeOfMode';
31 import {
34 - insertUpdateIntoFiber,
32 + enqueueUpdate,
33 processUpdateQueue,
36 -} from './ReactFiberUpdateQueue';
34 + createUpdate,
35 + ReplaceState,
36 + ForceUpdate,
37 +} from './ReactUpdateQueue';
38 +import {NoWork} from './ReactFiberExpirationTime';
39
40 const fakeInternalInstance = {};
41 const isArray = Array.isArray;
42
43 let didWarnAboutStateAssignmentForComponent;
42 -let didWarnAboutUndefinedDerivedState;
44 let didWarnAboutUninitializedState;
45 let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
46 let didWarnAboutLegacyLifecyclesAndDerivedState;
47 +let didWarnAboutUndefinedDerivedState;
48 +let warnOnUndefinedDerivedState;
49 let warnOnInvalidCallback;
50
51 if (__DEV__) {
52 didWarnAboutStateAssignmentForComponent = new Set();
50 - didWarnAboutUndefinedDerivedState = new Set();
53 didWarnAboutUninitializedState = new Set();
54 didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set();
55 didWarnAboutLegacyLifecyclesAndDerivedState = new Set();
56 + didWarnAboutUndefinedDerivedState = new Set();
57
58 const didWarnOnInvalidCallback = new Set();
59
@@ -71,6 +74,21 @@ if (__DEV__) {
74 }
75 };
76
77 + warnOnUndefinedDerivedState = function(workInProgress, partialState) {
78 + if (partialState === undefined) {
79 + const componentName = getComponentName(workInProgress) || 'Component';
80 + if (!didWarnAboutUndefinedDerivedState.has(componentName)) {
81 + didWarnAboutUndefinedDerivedState.add(componentName);
82 + warning(
83 + false,
84 + '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
85 + 'You have returned undefined.',
86 + componentName,
87 + );
88 + }
89 + }
90 + };
91 +
92 // This is so gross but it's at least non-critical and can be removed if
93 // it causes problems. This is meant to give a nicer error message for
94 // ReactDOM15.unstable_renderSubtreeIntoContainer(reactDOM16Component,
@@ -92,17 +110,43 @@ if (__DEV__) {
110 });
111 Object.freeze(fakeInternalInstance);
112 }
95 -function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array<mixed>) {
96 - const resultState = {};
97 - for (let i = 0; i < capturedValues.length; i++) {
98 - const capturedValue: CapturedValue<mixed> = (capturedValues[i]: any);
99 - const error = capturedValue.value;
100 - const partialState = ctor.getDerivedStateFromCatch.call(null, error);
101 - if (partialState !== null && partialState !== undefined) {
102 - Object.assign(resultState, partialState);
113 +
114 +export function applyDerivedStateFromProps(
115 + workInProgress: Fiber,
116 + getDerivedStateFromProps: (props: any, state: any) => any,
117 + nextProps: any,
118 +) {
119 + const prevState = workInProgress.memoizedState;
120 +
121 + if (__DEV__) {
122 + if (
123 + debugRenderPhaseSideEffects ||
124 + (debugRenderPhaseSideEffectsForStrictMode &&
125 + workInProgress.mode & StrictMode)
126 + ) {
127 + // Invoke the function an extra time to help detect side-effects.
128 + getDerivedStateFromProps(nextProps, prevState);
129 }
130 }
105 - return resultState;
131 +
132 + const partialState = getDerivedStateFromProps(nextProps, prevState);
133 +
134 + if (__DEV__) {
135 + warnOnUndefinedDerivedState(workInProgress, partialState);
136 + }
137 + // Merge the partial state and the previous state.
138 + const memoizedState =
139 + partialState === null || partialState === undefined
140 + ? prevState
141 + : Object.assign({}, prevState, partialState);
142 + workInProgress.memoizedState = memoizedState;
143 +
144 + // Once the update queue is empty, persist the derived state onto the
145 + // base state.
146 + const updateQueue = workInProgress.updateQueue;
147 + if (updateQueue !== null && updateQueue.expirationTime === NoWork) {
148 + updateQueue.baseState = memoizedState;
149 + }
150 }
151
152 export default function(
@@ -120,64 +164,57 @@ export default function(
164 hasContextChanged,
165 } = legacyContext;
166
123 - // Class component state updater
124 - const updater = {
167 + const classComponentUpdater = {
168 isMounted,
126 - enqueueSetState(instance, partialState, callback) {
127 - const fiber = ReactInstanceMap.get(instance);
128 - callback = callback === undefined ? null : callback;
129 - if (__DEV__) {
130 - warnOnInvalidCallback(callback, 'setState');
131 - }
169 + enqueueSetState(inst, payload, callback) {
170 + const fiber = ReactInstanceMap.get(inst);
171 const expirationTime = computeExpirationForFiber(fiber);
133 - const update = {
134 - expirationTime,
135 - partialState,
136 - callback,
137 - isReplace: false,
138 - isForced: false,
139 - capturedValue: null,
140 - next: null,
141 - };
142 - insertUpdateIntoFiber(fiber, update);
172 +
173 + const update = createUpdate(expirationTime);
174 + update.payload = payload;
175 + if (callback !== undefined && callback !== null) {
176 + if (__DEV__) {
177 + warnOnInvalidCallback(callback, 'setState');
178 + }
179 + update.callback = callback;
180 + }
181 +
182 + enqueueUpdate(fiber, update, expirationTime);
183 scheduleWork(fiber, expirationTime);
184 },
145 - enqueueReplaceState(instance, state, callback) {
146 - const fiber = ReactInstanceMap.get(instance);
147 - callback = callback === undefined ? null : callback;
148 - if (__DEV__) {
149 - warnOnInvalidCallback(callback, 'replaceState');
150 - }
185 + enqueueReplaceState(inst, payload, callback) {
186 + const fiber = ReactInstanceMap.get(inst);
187 const expirationTime = computeExpirationForFiber(fiber);
152 - const update = {
153 - expirationTime,
154 - partialState: state,
155 - callback,
156 - isReplace: true,
157 - isForced: false,
158 - capturedValue: null,
159 - next: null,
160 - };
161 - insertUpdateIntoFiber(fiber, update);
188 +
189 + const update = createUpdate(expirationTime);
190 + update.tag = ReplaceState;
191 + update.payload = payload;
192 +
193 + if (callback !== undefined && callback !== null) {
194 + if (__DEV__) {
195 + warnOnInvalidCallback(callback, 'replaceState');
196 + }
197 + update.callback = callback;
198 + }
199 +
200 + enqueueUpdate(fiber, update, expirationTime);
201 scheduleWork(fiber, expirationTime);
202 },
164 - enqueueForceUpdate(instance, callback) {
165 - const fiber = ReactInstanceMap.get(instance);
166 - callback = callback === undefined ? null : callback;
167 - if (__DEV__) {
168 - warnOnInvalidCallback(callback, 'forceUpdate');
169 - }
203 + enqueueForceUpdate(inst, callback) {
204 + const fiber = ReactInstanceMap.get(inst);
205 const expirationTime = computeExpirationForFiber(fiber);
171 - const update = {
172 - expirationTime,
173 - partialState: null,
174 - callback,
175 - isReplace: false,
176 - isForced: true,
177 - capturedValue: null,
178 - next: null,
179 - };
180 - insertUpdateIntoFiber(fiber, update);
206 +
207 + const update = createUpdate(expirationTime);
208 + update.tag = ForceUpdate;
209 +
210 + if (callback !== undefined && callback !== null) {
211 + if (__DEV__) {
212 + warnOnInvalidCallback(callback, 'forceUpdate');
213 + }
214 + update.callback = callback;
215 + }
216 +
217 + enqueueUpdate(fiber, update, expirationTime);
218 scheduleWork(fiber, expirationTime);
219 },
220 };
@@ -191,11 +228,10 @@ export default function(
228 newContext,
229 ) {
230 if (
194 - oldProps === null ||
195 - (workInProgress.updateQueue !== null &&
196 - workInProgress.updateQueue.hasForceUpdate)
231 + workInProgress.updateQueue !== null &&
232 + workInProgress.updateQueue.hasForceUpdate
233 ) {
198 - // If the workInProgress already has an Update effect, return true
234 + // If forceUpdate was called, disregard sCU.
235 return true;
236 }
237
@@ -420,13 +456,8 @@ export default function(
456 }
457 }
458
423 - function resetInputPointers(workInProgress: Fiber, instance: any) {
424 - instance.props = workInProgress.memoizedProps;
425 - instance.state = workInProgress.memoizedState;
426 - }
427 -
459 function adoptClassInstance(workInProgress: Fiber, instance: any): void {
429 - instance.updater = updater;
460 + instance.updater = classComponentUpdater;
461 workInProgress.stateNode = instance;
462 // The instance needs access to the fiber so that it can schedule updates
463 ReactInstanceMap.set(instance, workInProgress);
@@ -435,7 +466,11 @@ export default function(
466 }
467 }
468
438 - function constructClassInstance(workInProgress: Fiber, props: any): any {
469 + function constructClassInstance(
470 + workInProgress: Fiber,
471 + props: any,
472 + renderExpirationTime: ExpirationTime,
473 + ): any {
474 const ctor = workInProgress.type;
475 const unmaskedContext = getUnmaskedContext(workInProgress);
476 const needsContext = isContextConsumer(workInProgress);
@@ -444,19 +479,21 @@ export default function(
479 : emptyObject;
480
481 // Instantiate twice to help detect side-effects.
447 - if (
448 - debugRenderPhaseSideEffects ||
449 - (debugRenderPhaseSideEffectsForStrictMode &&
450 - workInProgress.mode & StrictMode)
451 - ) {
452 - new ctor(props, context); // eslint-disable-line no-new
482 + if (__DEV__) {
483 + if (
484 + debugRenderPhaseSideEffects ||
485 + (debugRenderPhaseSideEffectsForStrictMode &&
486 + workInProgress.mode & StrictMode)
487 + ) {
488 + new ctor(props, context); // eslint-disable-line no-new
489 + }
490 }
491
492 const instance = new ctor(props, context);
456 - const state =
493 + const state = (workInProgress.memoizedState =
494 instance.state !== null && instance.state !== undefined
495 ? instance.state
459 - : null;
496 + : null);
497 adoptClassInstance(workInProgress, instance);
498
499 if (__DEV__) {
@@ -545,26 +582,6 @@ export default function(
582 }
583 }
584
548 - workInProgress.memoizedState = state;
549 -
550 - const partialState = callGetDerivedStateFromProps(
551 - workInProgress,
552 - instance,
553 - props,
554 - state,
555 - );
556 -
557 - if (partialState !== null && partialState !== undefined) {
558 - // Render-phase updates (like this) should not be added to the update queue,
559 - // So that multiple render passes do not enqueue multiple updates.
560 - // Instead, just synchronously merge the returned state into the instance.
561 - workInProgress.memoizedState = Object.assign(
562 - {},
563 - workInProgress.memoizedState,
564 - partialState,
565 - );
566 - }
567 -
585 // Cache unmasked context so we can avoid recreating masked context unless necessary.
586 // ReactFiberContext usually updates this cache but can't for newly-created instances.
587 if (needsContext) {
@@ -597,7 +614,7 @@ export default function(
614 getComponentName(workInProgress) || 'Component',
615 );
616 }
600 - updater.enqueueReplaceState(instance, instance.state, null);
617 + classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
618 }
619 }
620
@@ -631,50 +648,7 @@ export default function(
648 );
649 }
650 }
634 - updater.enqueueReplaceState(instance, instance.state, null);
635 - }
636 - }
637 -
638 - function callGetDerivedStateFromProps(
639 - workInProgress: Fiber,
640 - instance: any,
641 - nextProps: any,
642 - prevState: any,
643 - ) {
644 - const {type} = workInProgress;
645 -
646 - if (typeof type.getDerivedStateFromProps === 'function') {
647 - if (
648 - debugRenderPhaseSideEffects ||
649 - (debugRenderPhaseSideEffectsForStrictMode &&
650 - workInProgress.mode & StrictMode)
651 - ) {
652 - // Invoke method an extra time to help detect side-effects.
653 - type.getDerivedStateFromProps.call(null, nextProps, prevState);
654 - }
655 -
656 - const partialState = type.getDerivedStateFromProps.call(
657 - null,
658 - nextProps,
659 - prevState,
660 - );
661 -
662 - if (__DEV__) {
663 - if (partialState === undefined) {
664 - const componentName = getComponentName(workInProgress) || 'Component';
665 - if (!didWarnAboutUndefinedDerivedState.has(componentName)) {
666 - didWarnAboutUndefinedDerivedState.add(componentName);
667 - warning(
668 - false,
669 - '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
670 - 'You have returned undefined.',
671 - componentName,
672 - );
673 - }
674 - }
675 - }
676 -
677 - return partialState;
651 + classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
652 }
653 }
654
@@ -684,7 +658,6 @@ export default function(
658 renderExpirationTime: ExpirationTime,
659 ): void {
660 const ctor = workInProgress.type;
687 - const current = workInProgress.alternate;
661
662 if (__DEV__) {
663 checkClassInstance(workInProgress);
@@ -715,6 +688,29 @@ export default function(
688 }
689 }
690
691 + let updateQueue = workInProgress.updateQueue;
692 + if (updateQueue !== null) {
693 + processUpdateQueue(
694 + workInProgress,
695 + updateQueue,
696 + props,
697 + instance,
698 + renderExpirationTime,
699 + );
700 + instance.state = workInProgress.memoizedState;
701 + }
702 +
703 + const getDerivedStateFromProps =
704 + workInProgress.type.getDerivedStateFromProps;
705 + if (typeof getDerivedStateFromProps === 'function') {
706 + applyDerivedStateFromProps(
707 + workInProgress,
708 + getDerivedStateFromProps,
709 + props,
710 + );
711 + instance.state = workInProgress.memoizedState;
712 + }
713 +
714 // In order to support react-lifecycles-compat polyfilled components,
715 // Unsafe lifecycles should not be invoked for components using the new APIs.
716 if (
@@ -726,18 +722,19 @@ export default function(
722 callComponentWillMount(workInProgress, instance);
723 // If we had additional state updates during this life-cycle, let's
724 // process them now.
729 - const updateQueue = workInProgress.updateQueue;
725 + updateQueue = workInProgress.updateQueue;
726 if (updateQueue !== null) {
731 - instance.state = processUpdateQueue(
732 - current,
727 + processUpdateQueue(
728 workInProgress,
729 updateQueue,
735 - instance,
730 props,
731 + instance,
732 renderExpirationTime,
733 );
734 + instance.state = workInProgress.memoizedState;
735 }
736 }
737 +
738 if (typeof instance.componentDidMount === 'function') {
739 workInProgress.effectTag |= Update;
740 }
@@ -749,16 +746,18 @@ export default function(
746 ): boolean {
747 const ctor = workInProgress.type;
748 const instance = workInProgress.stateNode;
752 - resetInputPointers(workInProgress, instance);
749
750 const oldProps = workInProgress.memoizedProps;
751 const newProps = workInProgress.pendingProps;
752 + instance.props = oldProps;
753 +
754 const oldContext = instance.context;
755 const newUnmaskedContext = getUnmaskedContext(workInProgress);
756 const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
757
758 + const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
759 const hasNewLifecycles =
761 - typeof ctor.getDerivedStateFromProps === 'function' ||
760 + typeof getDerivedStateFromProps === 'function' ||
761 typeof instance.getSnapshotBeforeUpdate === 'function';
762
763 // Note: During these life-cycles, instance.props/instance.state are what
@@ -782,93 +781,27 @@ export default function(
781 }
782 }
783
785 - // Compute the next state using the memoized state and the update queue.
784 const oldState = workInProgress.memoizedState;
787 - // TODO: Previous state can be null.
788 - let newState;
789 - let derivedStateFromCatch;
790 - if (workInProgress.updateQueue !== null) {
791 - newState = processUpdateQueue(
792 - null,
785 + let newState = (instance.state = oldState);
786 + let updateQueue = workInProgress.updateQueue;
787 + if (updateQueue !== null) {
788 + processUpdateQueue(
789 workInProgress,
794 - workInProgress.updateQueue,
795 - instance,
790 + updateQueue,
791 newProps,
792 + instance,
793 renderExpirationTime,
794 );
799 -
800 - let updateQueue = workInProgress.updateQueue;
801 - if (
802 - updateQueue !== null &&
803 - updateQueue.capturedValues !== null &&
804 - (enableGetDerivedStateFromCatch &&
805 - typeof ctor.getDerivedStateFromCatch === 'function')
806 - ) {
807 - const capturedValues = updateQueue.capturedValues;
808 - // Don't remove these from the update queue yet. We need them in
809 - // finishClassComponent. Do the reset there.
810 - // TODO: This is awkward. Refactor class components.
811 - // updateQueue.capturedValues = null;
812 - derivedStateFromCatch = callGetDerivedStateFromCatch(
813 - ctor,
814 - capturedValues,
815 - );
816 - }
817 - } else {
818 - newState = oldState;
795 + newState = workInProgress.memoizedState;
796 }
797
821 - let derivedStateFromProps;
822 - if (oldProps !== newProps) {
823 - // The prevState parameter should be the partially updated state.
824 - // Otherwise, spreading state in return values could override updates.
825 - derivedStateFromProps = callGetDerivedStateFromProps(
798 + if (typeof getDerivedStateFromProps === 'function') {
799 + applyDerivedStateFromProps(
800 workInProgress,
827 - instance,
801 + getDerivedStateFromProps,
802 newProps,
829 - newState,
803 );
831 - }
832 -
833 - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
834 - // Render-phase updates (like this) should not be added to the update queue,
835 - // So that multiple render passes do not enqueue multiple updates.
836 - // Instead, just synchronously merge the returned state into the instance.
837 - newState =
838 - newState === null || newState === undefined
839 - ? derivedStateFromProps
840 - : Object.assign({}, newState, derivedStateFromProps);
841 -
842 - // Update the base state of the update queue.
843 - // FIXME: This is getting ridiculous. Refactor plz!
844 - const updateQueue = workInProgress.updateQueue;
845 - if (updateQueue !== null) {
846 - updateQueue.baseState = Object.assign(
847 - {},
848 - updateQueue.baseState,
849 - derivedStateFromProps,
850 - );
851 - }
852 - }
853 - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) {
854 - // Render-phase updates (like this) should not be added to the update queue,
855 - // So that multiple render passes do not enqueue multiple updates.
856 - // Instead, just synchronously merge the returned state into the instance.
857 - newState =
858 - newState === null || newState === undefined
859 - ? derivedStateFromCatch
860 - : Object.assign({}, newState, derivedStateFromCatch);
861 -
862 - // Update the base state of the update queue.
863 - // FIXME: This is getting ridiculous. Refactor plz!
864 - const updateQueue = workInProgress.updateQueue;
865 - if (updateQueue !== null) {
866 - updateQueue.baseState = Object.assign(
867 - {},
868 - updateQueue.baseState,
869 - derivedStateFromCatch,
870 - );
871 - }
804 + newState = workInProgress.memoizedState;
805 }
806
807 if (
@@ -925,9 +858,9 @@ export default function(
858 }
859
860 // If shouldComponentUpdate returned false, we should still update the
928 - // memoized props/state to indicate that this work can be reused.
929 - memoizeProps(workInProgress, newProps);
930 - memoizeState(workInProgress, newState);
861 + // memoized state to indicate that this work can be reused.
862 + workInProgress.memoizedProps = newProps;
863 + workInProgress.memoizedState = newState;
864 }
865
866 // Update the existing instance's state, props, and context pointers even
@@ -947,16 +880,18 @@ export default function(
880 ): boolean {
881 const ctor = workInProgress.type;
882 const instance = workInProgress.stateNode;
950 - resetInputPointers(workInProgress, instance);
883
884 const oldProps = workInProgress.memoizedProps;
885 const newProps = workInProgress.pendingProps;
886 + instance.props = oldProps;
887 +
888 const oldContext = instance.context;
889 const newUnmaskedContext = getUnmaskedContext(workInProgress);
890 const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
891
892 + const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
893 const hasNewLifecycles =
959 - typeof ctor.getDerivedStateFromProps === 'function' ||
894 + typeof getDerivedStateFromProps === 'function' ||
895 typeof instance.getSnapshotBeforeUpdate === 'function';
896
897 // Note: During these life-cycles, instance.props/instance.state are what
@@ -980,94 +915,27 @@ export default function(
915 }
916 }
917
983 - // Compute the next state using the memoized state and the update queue.
918 const oldState = workInProgress.memoizedState;
985 - // TODO: Previous state can be null.
986 - let newState;
987 - let derivedStateFromCatch;
988 -
989 - if (workInProgress.updateQueue !== null) {
990 - newState = processUpdateQueue(
991 - current,
919 + let newState = (instance.state = oldState);
920 + let updateQueue = workInProgress.updateQueue;
921 + if (updateQueue !== null) {
922 + processUpdateQueue(
923 workInProgress,
993 - workInProgress.updateQueue,
994 - instance,
924 + updateQueue,
925 newProps,
926 + instance,
927 renderExpirationTime,
928 );
998 -
999 - let updateQueue = workInProgress.updateQueue;
1000 - if (
1001 - updateQueue !== null &&
1002 - updateQueue.capturedValues !== null &&
1003 - (enableGetDerivedStateFromCatch &&
1004 - typeof ctor.getDerivedStateFromCatch === 'function')
1005 - ) {
1006 - const capturedValues = updateQueue.capturedValues;
1007 - // Don't remove these from the update queue yet. We need them in
1008 - // finishClassComponent. Do the reset there.
1009 - // TODO: This is awkward. Refactor class components.
1010 - // updateQueue.capturedValues = null;
1011 - derivedStateFromCatch = callGetDerivedStateFromCatch(
1012 - ctor,
1013 - capturedValues,
1014 - );
1015 - }
1016 - } else {
1017 - newState = oldState;
929 + newState = workInProgress.memoizedState;
930 }
931
1020 - let derivedStateFromProps;
1021 - if (oldProps !== newProps) {
1022 - // The prevState parameter should be the partially updated state.
1023 - // Otherwise, spreading state in return values could override updates.
1024 - derivedStateFromProps = callGetDerivedStateFromProps(
932 + if (typeof getDerivedStateFromProps === 'function') {
933 + applyDerivedStateFromProps(
934 workInProgress,
1026 - instance,
935 + getDerivedStateFromProps,
936 newProps,
1028 - newState,
937 );
1030 - }
1031 -
1032 - if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
1033 - // Render-phase updates (like this) should not be added to the update queue,
1034 - // So that multiple render passes do not enqueue multiple updates.
1035 - // Instead, just synchronously merge the returned state into the instance.
1036 - newState =
1037 - newState === null || newState === undefined
1038 - ? derivedStateFromProps
1039 - : Object.assign({}, newState, derivedStateFromProps);
1040 -
1041 - // Update the base state of the update queue.
1042 - // FIXME: This is getting ridiculous. Refactor plz!
1043 - const updateQueue = workInProgress.updateQueue;
1044 - if (updateQueue !== null) {
1045 - updateQueue.baseState = Object.assign(
1046 - {},
1047 - updateQueue.baseState,
1048 - derivedStateFromProps,
1049 - );
1050 - }
1051 - }
1052 - if (derivedStateFromCatch !== null && derivedStateFromCatch !== undefined) {
1053 - // Render-phase updates (like this) should not be added to the update queue,
1054 - // So that multiple render passes do not enqueue multiple updates.
1055 - // Instead, just synchronously merge the returned state into the instance.
1056 - newState =
1057 - newState === null || newState === undefined
1058 - ? derivedStateFromCatch
1059 - : Object.assign({}, newState, derivedStateFromCatch);
1060 -
1061 - // Update the base state of the update queue.
1062 - // FIXME: This is getting ridiculous. Refactor plz!
1063 - const updateQueue = workInProgress.updateQueue;
1064 - if (updateQueue !== null) {
1065 - updateQueue.baseState = Object.assign(
1066 - {},
1067 - updateQueue.baseState,
1068 - derivedStateFromCatch,
1069 - );
1070 - }
938 + newState = workInProgress.memoizedState;
939 }
940
941 if (
@@ -1154,8 +1022,8 @@ export default function(
1022
1023 // If shouldComponentUpdate returned false, we should still update the
1024 // memoized props/state to indicate that this work can be reused.
1157 - memoizeProps(workInProgress, newProps);
1158 - memoizeState(workInProgress, newState);
1025 + workInProgress.memoizedProps = newProps;
1026 + workInProgress.memoizedState = newState;
1027 }
1028
1029 // Update the existing instance's state, props, and context pointers even
@@ -1169,7 +1037,6 @@ export default function(
1037
1038 return {
1039 adoptClassInstance,
1172 - callGetDerivedStateFromProps,
1040 constructClassInstance,
1041 mountClassInstance,
1042 resumeMountClassInstance,
packages/react-reconciler/src/ReactFiberCommitWork.js
+17 -74
@@ -33,15 +33,15 @@ import {
33 ContentReset,
34 Snapshot,
35 } from 'shared/ReactTypeOfSideEffect';
36 +import {commitUpdateQueue} from './ReactUpdateQueue';
37 import invariant from 'fbjs/lib/invariant';
38 import warning from 'fbjs/lib/warning';
39
39 -import {commitCallbacks} from './ReactFiberUpdateQueue';
40 import {onCommitUnmount} from './ReactFiberDevToolsHook';
41 import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
42 -import {logCapturedError} from './ReactFiberErrorLogger';
42 import getComponentName from 'shared/getComponentName';
43 import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
44 +import {logCapturedError} from './ReactFiberErrorLogger';
45
46 const {
47 invokeGuardedCallback,
@@ -54,7 +54,7 @@ if (__DEV__) {
54 didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
55 }
56
57 -function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
57 +export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
58 const source = errorInfo.source;
59 let stack = errorInfo.stack;
60 if (stack === null) {
@@ -251,7 +251,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
251 }
252 const updateQueue = finishedWork.updateQueue;
253 if (updateQueue !== null) {
254 - commitCallbacks(updateQueue, instance);
254 + instance.props = finishedWork.memoizedProps;
255 + instance.state = finishedWork.memoizedState;
256 + commitUpdateQueue(
257 + finishedWork,
258 + updateQueue,
259 + instance,
260 + committedExpirationTime,
261 + );
262 }
263 return;
264 }
@@ -269,7 +276,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
276 break;
277 }
278 }
272 - commitCallbacks(updateQueue, instance);
279 + commitUpdateQueue(
280 + finishedWork,
281 + updateQueue,
282 + instance,
283 + committedExpirationTime,
284 + );
285 }
286 return;
287 }
@@ -306,73 +318,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
318 }
319 }
320
309 - function commitErrorLogging(
310 - finishedWork: Fiber,
311 - onUncaughtError: (error: Error) => void,
312 - ) {
313 - switch (finishedWork.tag) {
314 - case ClassComponent:
315 - {
316 - const ctor = finishedWork.type;
317 - const instance = finishedWork.stateNode;
318 - const updateQueue = finishedWork.updateQueue;
319 - invariant(
320 - updateQueue !== null && updateQueue.capturedValues !== null,
321 - 'An error logging effect should not have been scheduled if no errors ' +
322 - 'were captured. This error is likely caused by a bug in React. ' +
323 - 'Please file an issue.',
324 - );
325 - const capturedErrors = updateQueue.capturedValues;
326 - updateQueue.capturedValues = null;
327 -
328 - if (typeof ctor.getDerivedStateFromCatch !== 'function') {
329 - // To preserve the preexisting retry behavior of error boundaries,
330 - // we keep track of which ones already failed during this batch.
331 - // This gets reset before we yield back to the browser.
332 - // TODO: Warn in strict mode if getDerivedStateFromCatch is
333 - // not defined.
334 - markLegacyErrorBoundaryAsFailed(instance);
335 - }
336 -
337 - instance.props = finishedWork.memoizedProps;
338 - instance.state = finishedWork.memoizedState;
339 - for (let i = 0; i < capturedErrors.length; i++) {
340 - const errorInfo = capturedErrors[i];
341 - const error = errorInfo.value;
342 - const stack = errorInfo.stack;
343 - logError(finishedWork, errorInfo);
344 - instance.componentDidCatch(error, {
345 - componentStack: stack !== null ? stack : '',
346 - });
347 - }
348 - }
349 - break;
350 - case HostRoot: {
351 - const updateQueue = finishedWork.updateQueue;
352 - invariant(
353 - updateQueue !== null && updateQueue.capturedValues !== null,
354 - 'An error logging effect should not have been scheduled if no errors ' +
355 - 'were captured. This error is likely caused by a bug in React. ' +
356 - 'Please file an issue.',
357 - );
358 - const capturedErrors = updateQueue.capturedValues;
359 - updateQueue.capturedValues = null;
360 - for (let i = 0; i < capturedErrors.length; i++) {
361 - const errorInfo = capturedErrors[i];
362 - logError(finishedWork, errorInfo);
363 - onUncaughtError(errorInfo.value);
364 - }
365 - break;
366 - }
367 - default:
368 - invariant(
369 - false,
370 - 'This unit of work tag cannot capture errors. This error is ' +
371 - 'likely caused by a bug in React. Please file an issue.',
372 - );
373 - }
374 - }
375 -
321 function commitAttachRef(finishedWork: Fiber) {
322 const ref = finishedWork.ref;
323 if (ref !== null) {
@@ -564,7 +509,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
509 },
510 commitLifeCycles,
511 commitBeforeMutationLifeCycles,
567 - commitErrorLogging,
512 commitAttachRef,
513 commitDetachRef,
514 };
@@ -892,7 +836,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
836 commitDeletion,
837 commitWork,
838 commitLifeCycles,
895 - commitErrorLogging,
839 commitAttachRef,
840 commitDetachRef,
841 };
packages/react-reconciler/src/ReactFiberCompleteWork.js
+1 -26
@@ -38,13 +38,7 @@ import {
38 Fragment,
39 Mode,
40 } from 'shared/ReactTypeOfWork';
41 -import {
42 - Placement,
43 - Ref,
44 - Update,
45 - ErrLog,
46 - DidCapture,
47 -} from 'shared/ReactTypeOfSideEffect';
41 +import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
42 import invariant from 'fbjs/lib/invariant';
43
44 import {reconcileChildFibers} from './ReactChildFiber';
@@ -416,20 +410,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
410 case ClassComponent: {
411 // We are leaving this subtree, so pop context if any.
412 popLegacyContextProvider(workInProgress);
419 -
420 - // If this component caught an error, schedule an error log effect.
421 - const instance = workInProgress.stateNode;
422 - const updateQueue = workInProgress.updateQueue;
423 - if (updateQueue !== null && updateQueue.capturedValues !== null) {
424 - workInProgress.effectTag &= ~DidCapture;
425 - if (typeof instance.componentDidCatch === 'function') {
426 - workInProgress.effectTag |= ErrLog;
427 - } else {
428 - // Normally we clear this in the commit phase, but since we did not
429 - // schedule an effect, we need to reset it here.
430 - updateQueue.capturedValues = null;
431 - }
432 - }
413 return null;
414 }
415 case HostRoot: {
@@ -449,11 +429,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
429 workInProgress.effectTag &= ~Placement;
430 }
431 updateHostContainer(workInProgress);
452 -
453 - const updateQueue = workInProgress.updateQueue;
454 - if (updateQueue !== null && updateQueue.capturedValues !== null) {
455 - workInProgress.effectTag |= ErrLog;
456 - }
432 return null;
433 }
434 case HostComponent: {
packages/react-reconciler/src/ReactFiberReconciler.js
+8 -14
@@ -26,7 +26,7 @@ import warning from 'fbjs/lib/warning';
26 import {createFiberRoot} from './ReactFiberRoot';
27 import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook';
28 import ReactFiberScheduler from './ReactFiberScheduler';
29 -import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue';
29 +import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
30 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
31 import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
32
@@ -339,28 +339,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
339 }
340 }
341
342 + const update = createUpdate(expirationTime);
343 + update.payload = {children: element};
344 +
345 callback = callback === undefined ? null : callback;
343 - if (__DEV__) {
346 + if (callback !== null) {
347 warning(
345 - callback === null || typeof callback === 'function',
348 + typeof callback === 'function',
349 'render(...): Expected the last optional `callback` argument to be a ' +
350 'function. Instead received: %s.',
351 callback,
352 );
353 + update.callback = callback;
354 }
355 + enqueueUpdate(current, update, expirationTime);
356
352 - const update = {
353 - expirationTime,
354 - partialState: {element},
355 - callback,
356 - isReplace: false,
357 - isForced: false,
358 - capturedValue: null,
359 - next: null,
360 - };
361 - insertUpdateIntoFiber(current, update);
357 scheduleWork(current, expirationTime);
363 -
358 return expirationTime;
359 }
360
packages/react-reconciler/src/ReactFiberScheduler.js
+54 -35
@@ -31,7 +31,6 @@ import {
31 Ref,
32 Incomplete,
33 HostEffectMask,
34 - ErrLog,
34 } from 'shared/ReactTypeOfSideEffect';
35 import {
36 HostRoot,
@@ -89,10 +88,7 @@ import {
88 import {AsyncMode} from './ReactTypeOfMode';
89 import ReactFiberLegacyContext from './ReactFiberContext';
90 import ReactFiberNewContext from './ReactFiberNewContext';
92 -import {
93 - getUpdateExpirationTime,
94 - insertUpdateIntoFiber,
95 -} from './ReactFiberUpdateQueue';
91 +import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
92 import {createCapturedValue} from './ReactCapturedValue';
93 import ReactFiberStack from './ReactFiberStack';
94
@@ -195,12 +191,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
191 throwException,
192 unwindWork,
193 unwindInterruptedWork,
194 + createRootErrorUpdate,
195 + createClassErrorUpdate,
196 } = ReactFiberUnwindWork(
197 hostContext,
198 legacyContext,
199 newContext,
200 scheduleWork,
201 + markLegacyErrorBoundaryAsFailed,
202 isAlreadyFailedLegacyErrorBoundary,
203 + onUncaughtError,
204 );
205 const {
206 commitBeforeMutationLifeCycles,
@@ -209,7 +209,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
209 commitDeletion,
210 commitWork,
211 commitLifeCycles,
212 - commitErrorLogging,
212 commitAttachRef,
213 commitDetachRef,
214 } = ReactFiberCommitWork(
@@ -447,10 +446,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
446 );
447 }
448
450 - if (effectTag & ErrLog) {
451 - commitErrorLogging(nextEffect, onUncaughtError);
452 - }
453 -
449 if (effectTag & Ref) {
450 recordEffect();
451 commitAttachRef(nextEffect);
@@ -681,7 +676,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
676 }
677
678 // Check for pending updates.
684 - let newExpirationTime = getUpdateExpirationTime(workInProgress);
679 + let newExpirationTime = NoWork;
680 + switch (workInProgress.tag) {
681 + case HostRoot:
682 + case ClassComponent: {
683 + const updateQueue = workInProgress.updateQueue;
684 + if (updateQueue !== null) {
685 + newExpirationTime = updateQueue.expirationTime;
686 + }
687 + }
688 + }
689
690 // TODO: Calls need to visit stateNode
691
@@ -956,6 +960,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
960 break;
961 }
962
963 + if (__DEV__) {
964 + // Reset global debug state
965 + // We assume this is defined in DEV
966 + (resetCurrentlyProcessingQueue: any)();
967 + }
968 +
969 if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
970 const failedUnitOfWork = nextUnitOfWork;
971 replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync);
@@ -974,7 +984,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
984 onUncaughtError(thrownValue);
985 break;
986 }
977 - throwException(returnFiber, sourceFiber, thrownValue);
987 + throwException(
988 + returnFiber,
989 + sourceFiber,
990 + thrownValue,
991 + nextRenderExpirationTime,
992 + );
993 nextUnitOfWork = completeUnitOfWork(sourceFiber);
994 }
995 break;
@@ -1022,22 +1037,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1037 }
1038 }
1039
1025 - function scheduleCapture(sourceFiber, boundaryFiber, value, expirationTime) {
1026 - // TODO: We only support dispatching errors.
1027 - const capturedValue = createCapturedValue(value, sourceFiber);
1028 - const update = {
1029 - expirationTime,
1030 - partialState: null,
1031 - callback: null,
1032 - isReplace: false,
1033 - isForced: false,
1034 - capturedValue,
1035 - next: null,
1036 - };
1037 - insertUpdateIntoFiber(boundaryFiber, update);
1038 - scheduleWork(boundaryFiber, expirationTime);
1039 - }
1040 -
1040 function dispatch(
1041 sourceFiber: Fiber,
1042 value: mixed,
@@ -1048,8 +1047,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1047 'dispatch: Cannot dispatch during the render phase.',
1048 );
1049
1051 - // TODO: Handle arrays
1052 -
1050 let fiber = sourceFiber.return;
1051 while (fiber !== null) {
1052 switch (fiber.tag) {
@@ -1061,14 +1058,28 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1058 (typeof instance.componentDidCatch === 'function' &&
1059 !isAlreadyFailedLegacyErrorBoundary(instance))
1060 ) {
1064 - scheduleCapture(sourceFiber, fiber, value, expirationTime);
1061 + const errorInfo = createCapturedValue(value, sourceFiber);
1062 + const update = createClassErrorUpdate(
1063 + fiber,
1064 + errorInfo,
1065 + expirationTime,
1066 + );
1067 + enqueueUpdate(fiber, update, expirationTime);
1068 + scheduleWork(fiber, expirationTime);
1069 return;
1070 }
1071 break;
1068 - // TODO: Handle async boundaries
1069 - case HostRoot:
1070 - scheduleCapture(sourceFiber, fiber, value, expirationTime);
1072 + case HostRoot: {
1073 + const errorInfo = createCapturedValue(value, sourceFiber);
1074 + const update = createRootErrorUpdate(
1075 + fiber,
1076 + errorInfo,
1077 + expirationTime,
1078 + );
1079 + enqueueUpdate(fiber, update, expirationTime);
1080 + scheduleWork(fiber, expirationTime);
1081 return;
1082 + }
1083 }
1084 fiber = fiber.return;
1085 }
@@ -1076,7 +1087,15 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1087 if (sourceFiber.tag === HostRoot) {
1088 // Error was thrown at the root. There is no parent, so the root
1089 // itself should capture it.
1079 - scheduleCapture(sourceFiber, sourceFiber, value, expirationTime);
1090 + const rootFiber = sourceFiber;
1091 + const errorInfo = createCapturedValue(value, rootFiber);
1092 + const update = createRootErrorUpdate(
1093 + rootFiber,
1094 + errorInfo,
1095 + expirationTime,
1096 + );
1097 + enqueueUpdate(rootFiber, update, expirationTime);
1098 + scheduleWork(rootFiber, expirationTime);
1099 }
1100 }
1101
packages/react-reconciler/src/ReactFiberUnwindWork.js
+87 -18
@@ -12,10 +12,16 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
12 import type {HostContext} from './ReactFiberHostContext';
13 import type {LegacyContext} from './ReactFiberContext';
14 import type {NewContext} from './ReactFiberNewContext';
15 -import type {UpdateQueue} from './ReactFiberUpdateQueue';
15 +import type {CapturedValue} from './ReactCapturedValue';
16 +import type {Update} from './ReactUpdateQueue';
17
18 import {createCapturedValue} from './ReactCapturedValue';
18 -import {ensureUpdateQueues} from './ReactFiberUpdateQueue';
19 +import {
20 + enqueueCapturedUpdate,
21 + createUpdate,
22 + CaptureUpdate,
23 +} from './ReactUpdateQueue';
24 +import {logError} from './ReactFiberCommitWork';
25
26 import {
27 ClassComponent,
@@ -42,7 +48,9 @@ export default function<C, CX>(
48 startTime: ExpirationTime,
49 expirationTime: ExpirationTime,
50 ) => void,
51 + markLegacyErrorBoundaryAsFailed: (instance: mixed) => void,
52 isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean,
53 + onUncaughtError: (error: mixed) => void,
54 ) {
55 const {popHostContainer, popHostContext} = hostContext;
56 const {
@@ -51,10 +59,71 @@ export default function<C, CX>(
59 } = legacyContext;
60 const {popProvider} = newContext;
61
62 + function createRootErrorUpdate(
63 + fiber: Fiber,
64 + errorInfo: CapturedValue<mixed>,
65 + expirationTime: ExpirationTime,
66 + ): Update<null> {
67 + const update = createUpdate(expirationTime);
68 + // Unmount the root by rendering null.
69 + update.tag = CaptureUpdate;
70 + update.payload = {children: null};
71 + const error = errorInfo.value;
72 + update.callback = () => {
73 + onUncaughtError(error);
74 + logError(fiber, errorInfo);
75 + };
76 + return update;
77 + }
78 +
79 + function createClassErrorUpdate(
80 + fiber: Fiber,
81 + errorInfo: CapturedValue<mixed>,
82 + expirationTime: ExpirationTime,
83 + ): Update<mixed> {
84 + const update = createUpdate(expirationTime);
85 + update.tag = CaptureUpdate;
86 + const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch;
87 + if (
88 + enableGetDerivedStateFromCatch &&
89 + typeof getDerivedStateFromCatch === 'function'
90 + ) {
91 + const error = errorInfo.value;
92 + update.payload = () => {
93 + return getDerivedStateFromCatch(error);
94 + };
95 + }
96 +
97 + const inst = fiber.stateNode;
98 + if (inst !== null && typeof inst.componentDidCatch === 'function') {
99 + update.callback = function callback() {
100 + if (
101 + !enableGetDerivedStateFromCatch ||
102 + getDerivedStateFromCatch !== 'function'
103 + ) {
104 + // To preserve the preexisting retry behavior of error boundaries,
105 + // we keep track of which ones already failed during this batch.
106 + // This gets reset before we yield back to the browser.
107 + // TODO: Warn in strict mode if getDerivedStateFromCatch is
108 + // not defined.
109 + markLegacyErrorBoundaryAsFailed(this);
110 + }
111 + const error = errorInfo.value;
112 + const stack = errorInfo.stack;
113 + logError(fiber, errorInfo);
114 + this.componentDidCatch(error, {
115 + componentStack: stack !== null ? stack : '',
116 + });
117 + };
118 + }
119 + return update;
120 + }
121 +
122 function throwException(
123 returnFiber: Fiber,
124 sourceFiber: Fiber,
125 rawValue: mixed,
126 + renderExpirationTime: ExpirationTime,
127 ) {
128 // The source fiber did not complete.
129 sourceFiber.effectTag |= Incomplete;
@@ -67,18 +136,19 @@ export default function<C, CX>(
136 do {
137 switch (workInProgress.tag) {
138 case HostRoot: {
70 - // Uncaught error
139 const errorInfo = value;
72 - ensureUpdateQueues(workInProgress);
73 - const updateQueue: UpdateQueue<
74 - any,
75 - > = (workInProgress.updateQueue: any);
76 - updateQueue.capturedValues = [errorInfo];
140 workInProgress.effectTag |= ShouldCapture;
141 + const update = createRootErrorUpdate(
142 + workInProgress,
143 + errorInfo,
144 + renderExpirationTime,
145 + );
146 + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime);
147 return;
148 }
149 case ClassComponent:
150 // Capture and retry
151 + const errorInfo = value;
152 const ctor = workInProgress.type;
153 const instance = workInProgress.stateNode;
154 if (
@@ -89,17 +159,14 @@ export default function<C, CX>(
159 typeof instance.componentDidCatch === 'function' &&
160 !isAlreadyFailedLegacyErrorBoundary(instance)))
161 ) {
92 - ensureUpdateQueues(workInProgress);
93 - const updateQueue: UpdateQueue<
94 - any,
95 - > = (workInProgress.updateQueue: any);
96 - const capturedValues = updateQueue.capturedValues;
97 - if (capturedValues === null) {
98 - updateQueue.capturedValues = [value];
99 - } else {
100 - capturedValues.push(value);
101 - }
162 workInProgress.effectTag |= ShouldCapture;
163 + // Schedule the error boundary to re-render using updated state
164 + const update = createClassErrorUpdate(
165 + workInProgress,
166 + errorInfo,
167 + renderExpirationTime,
168 + );
169 + enqueueCapturedUpdate(workInProgress, update, renderExpirationTime);
170 return;
171 }
172 break;
@@ -176,5 +243,7 @@ export default function<C, CX>(
243 throwException,
244 unwindWork,
245 unwindInterruptedWork,
246 + createRootErrorUpdate,
247 + createClassErrorUpdate,
248 };
249 }
packages/react-reconciler/src/ReactFiberUpdateQueue.js deleted
-394
@@ -1,394 +0,0 @@
1 -/**
2 - * Copyright (c) 2013-present, Facebook, Inc.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {Fiber} from './ReactFiber';
11 -import type {ExpirationTime} from './ReactFiberExpirationTime';
12 -import type {CapturedValue} from './ReactCapturedValue';
13 -
14 -import {
15 - debugRenderPhaseSideEffects,
16 - debugRenderPhaseSideEffectsForStrictMode,
17 -} from 'shared/ReactFeatureFlags';
18 -import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect';
19 -import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork';
20 -import invariant from 'fbjs/lib/invariant';
21 -import warning from 'fbjs/lib/warning';
22 -import {StrictMode} from './ReactTypeOfMode';
23 -
24 -import {NoWork} from './ReactFiberExpirationTime';
25 -
26 -let didWarnUpdateInsideUpdate;
27 -
28 -if (__DEV__) {
29 - didWarnUpdateInsideUpdate = false;
30 -}
31 -
32 -type PartialState<State, Props> =
33 - | $Subtype<State>
34 - | ((prevState: State, props: Props) => $Subtype<State>);
35 -
36 -// Callbacks are not validated until invocation
37 -type Callback = mixed;
38 -
39 -export type Update<State> = {
40 - expirationTime: ExpirationTime,
41 - partialState: PartialState<any, any>,
42 - callback: Callback | null,
43 - isReplace: boolean,
44 - isForced: boolean,
45 - capturedValue: CapturedValue<mixed> | null,
46 - next: Update<State> | null,
47 -};
48 -
49 -// Singly linked-list of updates. When an update is scheduled, it is added to
50 -// the queue of the current fiber and the work-in-progress fiber. The two queues
51 -// are separate but they share a persistent structure.
52 -//
53 -// During reconciliation, updates are removed from the work-in-progress fiber,
54 -// but they remain on the current fiber. That ensures that if a work-in-progress
55 -// is aborted, the aborted updates are recovered by cloning from current.
56 -//
57 -// The work-in-progress queue is always a subset of the current queue.
58 -//
59 -// When the tree is committed, the work-in-progress becomes the current.
60 -export type UpdateQueue<State> = {
61 - // A processed update is not removed from the queue if there are any
62 - // unprocessed updates that came before it. In that case, we need to keep
63 - // track of the base state, which represents the base state of the first
64 - // unprocessed update, which is the same as the first update in the list.
65 - baseState: State,
66 - // For the same reason, we keep track of the remaining expiration time.
67 - expirationTime: ExpirationTime,
68 - first: Update<State> | null,
69 - last: Update<State> | null,
70 - callbackList: Array<Update<State>> | null,
71 - hasForceUpdate: boolean,
72 - isInitialized: boolean,
73 - capturedValues: Array<CapturedValue<mixed>> | null,
74 -
75 - // Dev only
76 - isProcessing?: boolean,
77 -};
78 -
79 -function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
80 - const queue: UpdateQueue<State> = {
81 - baseState,
82 - expirationTime: NoWork,
83 - first: null,
84 - last: null,
85 - callbackList: null,
86 - hasForceUpdate: false,
87 - isInitialized: false,
88 - capturedValues: null,
89 - };
90 - if (__DEV__) {
91 - queue.isProcessing = false;
92 - }
93 - return queue;
94 -}
95 -
96 -export function insertUpdateIntoQueue<State>(
97 - queue: UpdateQueue<State>,
98 - update: Update<State>,
99 -): void {
100 - // Append the update to the end of the list.
101 - if (queue.last === null) {
102 - // Queue is empty
103 - queue.first = queue.last = update;
104 - } else {
105 - queue.last.next = update;
106 - queue.last = update;
107 - }
108 - if (
109 - queue.expirationTime === NoWork ||
110 - queue.expirationTime > update.expirationTime
111 - ) {
112 - queue.expirationTime = update.expirationTime;
113 - }
114 -}
115 -
116 -let q1;
117 -let q2;
118 -export function ensureUpdateQueues(fiber: Fiber) {
119 - q1 = q2 = null;
120 - // We'll have at least one and at most two distinct update queues.
121 - const alternateFiber = fiber.alternate;
122 - let queue1 = fiber.updateQueue;
123 - if (queue1 === null) {
124 - // TODO: We don't know what the base state will be until we begin work.
125 - // It depends on which fiber is the next current. Initialize with an empty
126 - // base state, then set to the memoizedState when rendering. Not super
127 - // happy with this approach.
128 - queue1 = fiber.updateQueue = createUpdateQueue((null: any));
129 - }
130 -
131 - let queue2;
132 - if (alternateFiber !== null) {
133 - queue2 = alternateFiber.updateQueue;
134 - if (queue2 === null) {
135 - queue2 = alternateFiber.updateQueue = createUpdateQueue((null: any));
136 - }
137 - } else {
138 - queue2 = null;
139 - }
140 - queue2 = queue2 !== queue1 ? queue2 : null;
141 -
142 - // Use module variables instead of returning a tuple
143 - q1 = queue1;
144 - q2 = queue2;
145 -}
146 -
147 -export function insertUpdateIntoFiber<State>(
148 - fiber: Fiber,
149 - update: Update<State>,
150 -): void {
151 - ensureUpdateQueues(fiber);
152 - const queue1: Fiber = (q1: any);
153 - const queue2: Fiber | null = (q2: any);
154 -
155 - // Warn if an update is scheduled from inside an updater function.
156 - if (__DEV__) {
157 - if (
158 - (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) &&
159 - !didWarnUpdateInsideUpdate
160 - ) {
161 - warning(
162 - false,
163 - 'An update (setState, replaceState, or forceUpdate) was scheduled ' +
164 - 'from inside an update function. Update functions should be pure, ' +
165 - 'with zero side-effects. Consider using componentDidUpdate or a ' +
166 - 'callback.',
167 - );
168 - didWarnUpdateInsideUpdate = true;
169 - }
170 - }
171 -
172 - // If there's only one queue, add the update to that queue and exit.
173 - if (queue2 === null) {
174 - insertUpdateIntoQueue(queue1, update);
175 - return;
176 - }
177 -
178 - // If either queue is empty, we need to add to both queues.
179 - if (queue1.last === null || queue2.last === null) {
180 - insertUpdateIntoQueue(queue1, update);
181 - insertUpdateIntoQueue(queue2, update);
182 - return;
183 - }
184 -
185 - // If both lists are not empty, the last update is the same for both lists
186 - // because of structural sharing. So, we should only append to one of
187 - // the lists.
188 - insertUpdateIntoQueue(queue1, update);
189 - // But we still need to update the `last` pointer of queue2.
190 - queue2.last = update;
191 -}
192 -
193 -export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
194 - switch (fiber.tag) {
195 - case HostRoot:
196 - case ClassComponent:
197 - const updateQueue = fiber.updateQueue;
198 - if (updateQueue === null) {
199 - return NoWork;
200 - }
201 - return updateQueue.expirationTime;
202 - default:
203 - return NoWork;
204 - }
205 -}
206 -
207 -function getStateFromUpdate(update, instance, prevState, props) {
208 - const partialState = update.partialState;
209 - if (typeof partialState === 'function') {
210 - return partialState.call(instance, prevState, props);
211 - } else {
212 - return partialState;
213 - }
214 -}
215 -
216 -export function processUpdateQueue<State>(
217 - current: Fiber | null,
218 - workInProgress: Fiber,
219 - queue: UpdateQueue<State>,
220 - instance: any,
221 - props: any,
222 - renderExpirationTime: ExpirationTime,
223 -): State {
224 - if (current !== null && current.updateQueue === queue) {
225 - // We need to create a work-in-progress queue, by cloning the current queue.
226 - const currentQueue = queue;
227 - queue = workInProgress.updateQueue = {
228 - baseState: currentQueue.baseState,
229 - expirationTime: currentQueue.expirationTime,
230 - first: currentQueue.first,
231 - last: currentQueue.last,
232 - isInitialized: currentQueue.isInitialized,
233 - capturedValues: currentQueue.capturedValues,
234 - // These fields are no longer valid because they were already committed.
235 - // Reset them.
236 - callbackList: null,
237 - hasForceUpdate: false,
238 - };
239 - }
240 -
241 - if (__DEV__) {
242 - // Set this flag so we can warn if setState is called inside the update
243 - // function of another setState.
244 - queue.isProcessing = true;
245 - }
246 -
247 - // Reset the remaining expiration time. If we skip over any updates, we'll
248 - // increase this accordingly.
249 - queue.expirationTime = NoWork;
250 -
251 - // TODO: We don't know what the base state will be until we begin work.
252 - // It depends on which fiber is the next current. Initialize with an empty
253 - // base state, then set to the memoizedState when rendering. Not super
254 - // happy with this approach.
255 - let state;
256 - if (queue.isInitialized) {
257 - state = queue.baseState;
258 - } else {
259 - state = queue.baseState = workInProgress.memoizedState;
260 - queue.isInitialized = true;
261 - }
262 - let dontMutatePrevState = true;
263 - let update = queue.first;
264 - let didSkip = false;
265 - while (update !== null) {
266 - const updateExpirationTime = update.expirationTime;
267 - if (updateExpirationTime > renderExpirationTime) {
268 - // This update does not have sufficient priority. Skip it.
269 - const remainingExpirationTime = queue.expirationTime;
270 - if (
271 - remainingExpirationTime === NoWork ||
272 - remainingExpirationTime > updateExpirationTime
273 - ) {
274 - // Update the remaining expiration time.
275 - queue.expirationTime = updateExpirationTime;
276 - }
277 - if (!didSkip) {
278 - didSkip = true;
279 - queue.baseState = state;
280 - }
281 - // Continue to the next update.
282 - update = update.next;
283 - continue;
284 - }
285 -
286 - // This update does have sufficient priority.
287 -
288 - // If no previous updates were skipped, drop this update from the queue by
289 - // advancing the head of the list.
290 - if (!didSkip) {
291 - queue.first = update.next;
292 - if (queue.first === null) {
293 - queue.last = null;
294 - }
295 - }
296 -
297 - // Invoke setState callback an extra time to help detect side-effects.
298 - // Ignore the return value in this case.
299 - if (
300 - debugRenderPhaseSideEffects ||
301 - (debugRenderPhaseSideEffectsForStrictMode &&
302 - workInProgress.mode & StrictMode)
303 - ) {
304 - getStateFromUpdate(update, instance, state, props);
305 - }
306 -
307 - // Process the update
308 - let partialState;
309 - if (update.isReplace) {
310 - state = getStateFromUpdate(update, instance, state, props);
311 - dontMutatePrevState = true;
312 - } else {
313 - partialState = getStateFromUpdate(update, instance, state, props);
314 - if (partialState) {
315 - if (dontMutatePrevState) {
316 - // $FlowFixMe: Idk how to type this properly.
317 - state = Object.assign({}, state, partialState);
318 - } else {
319 - state = Object.assign(state, partialState);
320 - }
321 - dontMutatePrevState = false;
322 - }
323 - }
324 - if (update.isForced) {
325 - queue.hasForceUpdate = true;
326 - }
327 - if (update.callback !== null) {
328 - // Append to list of callbacks.
329 - let callbackList = queue.callbackList;
330 - if (callbackList === null) {
331 - callbackList = queue.callbackList = [];
332 - }
333 - callbackList.push(update);
334 - }
335 - if (update.capturedValue !== null) {
336 - let capturedValues = queue.capturedValues;
337 - if (capturedValues === null) {
338 - queue.capturedValues = [update.capturedValue];
339 - } else {
340 - capturedValues.push(update.capturedValue);
341 - }
342 - }
343 - update = update.next;
344 - }
345 -
346 - if (queue.callbackList !== null) {
347 - workInProgress.effectTag |= CallbackEffect;
348 - } else if (
349 - queue.first === null &&
350 - !queue.hasForceUpdate &&
351 - queue.capturedValues === null
352 - ) {
353 - // The queue is empty. We can reset it.
354 - workInProgress.updateQueue = null;
355 - }
356 -
357 - if (!didSkip) {
358 - didSkip = true;
359 - queue.baseState = state;
360 - }
361 -
362 - if (__DEV__) {
363 - // No longer processing.
364 - queue.isProcessing = false;
365 - }
366 -
367 - return state;
368 -}
369 -
370 -export function commitCallbacks<State>(
371 - queue: UpdateQueue<State>,
372 - context: any,
373 -) {
374 - const callbackList = queue.callbackList;
375 - if (callbackList === null) {
376 - return;
377 - }
378 - // Set the list to null to make sure they don't get called more than once.
379 - queue.callbackList = null;
380 - for (let i = 0; i < callbackList.length; i++) {
381 - const update = callbackList[i];
382 - const callback = update.callback;
383 - // This update might be processed again. Clear the callback so it's only
384 - // called once.
385 - update.callback = null;
386 - invariant(
387 - typeof callback === 'function',
388 - 'Invalid argument passed as callback. Expected a function. Instead ' +
389 - 'received: %s',
390 - callback,
391 - );
392 - callback.call(context);
393 - }
394 -}
packages/react-reconciler/src/ReactUpdateQueue.js new
+640
@@ -0,0 +1,640 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +// UpdateQueue is a linked list of prioritized updates.
11 +//
12 +// Like fibers, update queues come in pairs: a current queue, which represents
13 +// the visible state of the screen, and a work-in-progress queue, which is
14 +// can be mutated and processed asynchronously before it is committed — a form
15 +// of double buffering. If a work-in-progress render is discarded before
16 +// finishing, we create a new work-in-progress by cloning the current queue.
17 +//
18 +// Both queues share a persistent, singly-linked list structure. To schedule an
19 +// update, we append it to the end of both queues. Each queue maintains a
20 +// pointer to first update in the persistent list that hasn't been processed.
21 +// The work-in-progress pointer always has a position equal to or greater than
22 +// the current queue, since we always work on that one. The current queue's
23 +// pointer is only updated during the commit phase, when we swap in the
24 +// work-in-progress.
25 +//
26 +// For example:
27 +//
28 +// Current pointer: A - B - C - D - E - F
29 +// Work-in-progress pointer: D - E - F
30 +// ^
31 +// The work-in-progress queue has
32 +// processed more updates than current.
33 +//
34 +// The reason we append to both queues is because otherwise we might drop
35 +// updates without ever processing them. For example, if we only add updates to
36 +// the work-in-progress queue, some updates could be lost whenever a work-in
37 +// -progress render restarts by cloning from current. Similarly, if we only add
38 +// updates to the current queue, the updates will be lost whenever an already
39 +// in-progress queue commits and swaps with the current queue. However, by
40 +// adding to both queues, we guarantee that the update will be part of the next
41 +// work-in-progress. (And because the work-in-progress queue becomes the
42 +// current queue once it commits, there's no danger of applying the same
43 +// update twice.)
44 +//
45 +// Prioritization
46 +// --------------
47 +//
48 +// Updates are not sorted by priority, but by insertion; new updates are always
49 +// appended to the end of the list.
50 +//
51 +// The priority is still important, though. When processing the update queue
52 +// during the render phase, only the updates with sufficient priority are
53 +// included in the result. If we skip an update because it has insufficient
54 +// priority, it remains in the queue to be processed later, during a lower
55 +// priority render. Crucially, all updates subsequent to a skipped update also
56 +// remain in the queue *regardless of their priority*. That means high priority
57 +// updates are sometimes processed twice, at two separate priorities. We also
58 +// keep track of a base state, that represents the state before the first
59 +// update in the queue is applied.
60 +//
61 +// For example:
62 +//
63 +// Given a base state of '', and the following queue of updates
64 +//
65 +// A1 - B2 - C1 - D2
66 +//
67 +// where the number indicates the priority, and the update is applied to the
68 +// previous state by appending a letter, React will process these updates as
69 +// two separate renders, one per distinct priority level:
70 +//
71 +// First render, at priority 1:
72 +// Base state: ''
73 +// Updates: [A1, C1]
74 +// Result state: 'AC'
75 +//
76 +// Second render, at priority 2:
77 +// Base state: 'A' <- The base state does not include C1,
78 +// because B2 was skipped.
79 +// Updates: [B2, C1, D2] <- C1 was rebased on top of B2
80 +// Result state: 'ABCD'
81 +//
82 +// Because we process updates in insertion order, and rebase high priority
83 +// updates when preceding updates are skipped, the final result is deterministic
84 +// regardless of priority. Intermediate state may vary according to system
85 +// resources, but the final state is always the same.
86 +
87 +import type {Fiber} from './ReactFiber';
88 +import type {ExpirationTime} from './ReactFiberExpirationTime';
89 +
90 +import {NoWork} from './ReactFiberExpirationTime';
91 +import {
92 + Callback,
93 + ShouldCapture,
94 + DidCapture,
95 +} from 'shared/ReactTypeOfSideEffect';
96 +import {ClassComponent} from 'shared/ReactTypeOfWork';
97 +
98 +import {
99 + debugRenderPhaseSideEffects,
100 + debugRenderPhaseSideEffectsForStrictMode,
101 +} from 'shared/ReactFeatureFlags';
102 +
103 +import {StrictMode} from './ReactTypeOfMode';
104 +
105 +import invariant from 'fbjs/lib/invariant';
106 +import warning from 'fbjs/lib/warning';
107 +
108 +export type Update<State> = {
109 + expirationTime: ExpirationTime,
110 +
111 + tag: 0 | 1 | 2 | 3,
112 + payload: any,
113 + callback: (() => mixed) | null,
114 +
115 + next: Update<State> | null,
116 + nextEffect: Update<State> | null,
117 +};
118 +
119 +export type UpdateQueue<State> = {
120 + expirationTime: ExpirationTime,
121 + baseState: State,
122 +
123 + firstUpdate: Update<State> | null,
124 + lastUpdate: Update<State> | null,
125 +
126 + firstCapturedUpdate: Update<State> | null,
127 + lastCapturedUpdate: Update<State> | null,
128 +
129 + firstEffect: Update<State> | null,
130 + lastEffect: Update<State> | null,
131 +
132 + firstCapturedEffect: Update<State> | null,
133 + lastCapturedEffect: Update<State> | null,
134 +
135 + // TODO: Workaround for lack of tuples. Could use global state instead.
136 + hasForceUpdate: boolean,
137 +};
138 +
139 +export const UpdateState = 0;
140 +export const ReplaceState = 1;
141 +export const ForceUpdate = 2;
142 +export const CaptureUpdate = 3;
143 +
144 +let didWarnUpdateInsideUpdate;
145 +let currentlyProcessingQueue;
146 +export let resetCurrentlyProcessingQueue;
147 +if (__DEV__) {
148 + didWarnUpdateInsideUpdate = false;
149 + currentlyProcessingQueue = null;
150 + resetCurrentlyProcessingQueue = () => {
151 + currentlyProcessingQueue = null;
152 + };
153 +}
154 +
155 +export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
156 + const queue: UpdateQueue<State> = {
157 + expirationTime: NoWork,
158 + baseState,
159 + firstUpdate: null,
160 + lastUpdate: null,
161 + firstCapturedUpdate: null,
162 + lastCapturedUpdate: null,
163 + firstEffect: null,
164 + lastEffect: null,
165 + firstCapturedEffect: null,
166 + lastCapturedEffect: null,
167 + hasForceUpdate: false,
168 + };
169 + return queue;
170 +}
171 +
172 +function cloneUpdateQueue<State>(
173 + currentQueue: UpdateQueue<State>,
174 +): UpdateQueue<State> {
175 + const queue: UpdateQueue<State> = {
176 + expirationTime: currentQueue.expirationTime,
177 + baseState: currentQueue.baseState,
178 + firstUpdate: currentQueue.firstUpdate,
179 + lastUpdate: currentQueue.lastUpdate,
180 +
181 + // TODO: With resuming, if we bail out and resuse the child tree, we should
182 + // keep these effects.
183 + firstCapturedUpdate: null,
184 + lastCapturedUpdate: null,
185 +
186 + hasForceUpdate: false,
187 +
188 + firstEffect: null,
189 + lastEffect: null,
190 +
191 + firstCapturedEffect: null,
192 + lastCapturedEffect: null,
193 + };
194 + return queue;
195 +}
196 +
197 +export function createUpdate(expirationTime: ExpirationTime): Update<*> {
198 + return {
199 + expirationTime: expirationTime,
200 +
201 + tag: UpdateState,
202 + payload: null,
203 + callback: null,
204 +
205 + next: null,
206 + nextEffect: null,
207 + };
208 +}
209 +
210 +function appendUpdateToQueue<State>(
211 + queue: UpdateQueue<State>,
212 + update: Update<State>,
213 + expirationTime: ExpirationTime,
214 +) {
215 + // Append the update to the end of the list.
216 + if (queue.lastUpdate === null) {
217 + // Queue is empty
218 + queue.firstUpdate = queue.lastUpdate = update;
219 + } else {
220 + queue.lastUpdate.next = update;
221 + queue.lastUpdate = update;
222 + }
223 + if (
224 + queue.expirationTime === NoWork ||
225 + queue.expirationTime > expirationTime
226 + ) {
227 + // The incoming update has the earliest expiration of any update in the
228 + // queue. Update the queue's expiration time.
229 + queue.expirationTime = expirationTime;
230 + }
231 +}
232 +
233 +export function enqueueUpdate<State>(
234 + fiber: Fiber,
235 + update: Update<State>,
236 + expirationTime: ExpirationTime,
237 +) {
238 + // Update queues are created lazily.
239 + const alternate = fiber.alternate;
240 + let queue1;
241 + let queue2;
242 + if (alternate === null) {
243 + // There's only one fiber.
244 + queue1 = fiber.updateQueue;
245 + queue2 = null;
246 + if (queue1 === null) {
247 + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
248 + }
249 + } else {
250 + // There are two owners.
251 + queue1 = fiber.updateQueue;
252 + queue2 = alternate.updateQueue;
253 + if (queue1 === null) {
254 + if (queue2 === null) {
255 + // Neither fiber has an update queue. Create new ones.
256 + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
257 + queue2 = alternate.updateQueue = createUpdateQueue(
258 + alternate.memoizedState,
259 + );
260 + } else {
261 + // Only one fiber has an update queue. Clone to create a new one.
262 + queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
263 + }
264 + } else {
265 + if (queue2 === null) {
266 + // Only one fiber has an update queue. Clone to create a new one.
267 + queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
268 + } else {
269 + // Both owners have an update queue.
270 + }
271 + }
272 + }
273 + if (queue2 === null || queue1 === queue2) {
274 + // There's only a single queue.
275 + appendUpdateToQueue(queue1, update, expirationTime);
276 + } else {
277 + // There are two queues. We need to append the update to both queues,
278 + // while accounting for the persistent structure of the list — we don't
279 + // want the same update to be added multiple times.
280 + if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
281 + // One of the queues is not empty. We must add the update to both queues.
282 + appendUpdateToQueue(queue1, update, expirationTime);
283 + appendUpdateToQueue(queue2, update, expirationTime);
284 + } else {
285 + // Both queues are non-empty. The last update is the same in both lists,
286 + // because of structural sharing. So, only append to one of the lists.
287 + appendUpdateToQueue(queue1, update, expirationTime);
288 + // But we still need to update the `lastUpdate` pointer of queue2.
289 + queue2.lastUpdate = update;
290 + }
291 + }
292 +
293 + if (__DEV__) {
294 + if (
295 + fiber.tag === ClassComponent &&
296 + (currentlyProcessingQueue === queue1 ||
297 + (queue2 !== null && currentlyProcessingQueue === queue2)) &&
298 + !didWarnUpdateInsideUpdate
299 + ) {
300 + warning(
301 + false,
302 + 'An update (setState, replaceState, or forceUpdate) was scheduled ' +
303 + 'from inside an update function. Update functions should be pure, ' +
304 + 'with zero side-effects. Consider using componentDidUpdate or a ' +
305 + 'callback.',
306 + );
307 + didWarnUpdateInsideUpdate = true;
308 + }
309 + }
310 +}
311 +
312 +export function enqueueCapturedUpdate<State>(
313 + workInProgress: Fiber,
314 + update: Update<State>,
315 + renderExpirationTime: ExpirationTime,
316 +) {
317 + // Captured updates go into a separate list, and only on the work-in-
318 + // progress queue.
319 + let workInProgressQueue = workInProgress.updateQueue;
320 + if (workInProgressQueue === null) {
321 + workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
322 + workInProgress.memoizedState,
323 + );
324 + } else {
325 + // TODO: I put this here rather than createWorkInProgress so that we don't
326 + // clone the queue unnecessarily. There's probably a better way to
327 + // structure this.
328 + workInProgressQueue = ensureWorkInProgressQueueIsAClone(
329 + workInProgress,
330 + workInProgressQueue,
331 + );
332 + }
333 +
334 + // Append the update to the end of the list.
335 + if (workInProgressQueue.lastCapturedUpdate === null) {
336 + // This is the first render phase update
337 + workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
338 + } else {
339 + workInProgressQueue.lastCapturedUpdate.next = update;
340 + workInProgressQueue.lastCapturedUpdate = update;
341 + }
342 + if (
343 + workInProgressQueue.expirationTime === NoWork ||
344 + workInProgressQueue.expirationTime > renderExpirationTime
345 + ) {
346 + // The incoming update has the earliest expiration of any update in the
347 + // queue. Update the queue's expiration time.
348 + workInProgressQueue.expirationTime = renderExpirationTime;
349 + }
350 +}
351 +
352 +function ensureWorkInProgressQueueIsAClone<State>(
353 + workInProgress: Fiber,
354 + queue: UpdateQueue<State>,
355 +): UpdateQueue<State> {
356 + const current = workInProgress.alternate;
357 + if (current !== null) {
358 + // If the work-in-progress queue is equal to the current queue,
359 + // we need to clone it first.
360 + if (queue === current.updateQueue) {
361 + queue = workInProgress.updateQueue = cloneUpdateQueue(queue);
362 + }
363 + }
364 + return queue;
365 +}
366 +
367 +function getStateFromUpdate<State>(
368 + workInProgress: Fiber,
369 + queue: UpdateQueue<State>,
370 + update: Update<State>,
371 + prevState: State,
372 + nextProps: any,
373 + instance: any,
374 +): any {
375 + switch (update.tag) {
376 + case ReplaceState: {
377 + const payload = update.payload;
378 + if (typeof payload === 'function') {
379 + // Updater function
380 + if (__DEV__) {
381 + if (
382 + debugRenderPhaseSideEffects ||
383 + (debugRenderPhaseSideEffectsForStrictMode &&
384 + workInProgress.mode & StrictMode)
385 + ) {
386 + payload.call(instance, prevState, nextProps);
387 + }
388 + }
389 + return payload.call(instance, prevState, nextProps);
390 + }
391 + // State object
392 + return payload;
393 + }
394 + case CaptureUpdate: {
395 + workInProgress.effectTag =
396 + (workInProgress.effectTag & ~ShouldCapture) | DidCapture;
397 + }
398 + // Intentional fallthrough
399 + case UpdateState: {
400 + const payload = update.payload;
401 + let partialState;
402 + if (typeof payload === 'function') {
403 + // Updater function
404 + if (__DEV__) {
405 + if (
406 + debugRenderPhaseSideEffects ||
407 + (debugRenderPhaseSideEffectsForStrictMode &&
408 + workInProgress.mode & StrictMode)
409 + ) {
410 + payload.call(instance, prevState, nextProps);
411 + }
412 + }
413 + partialState = payload.call(instance, prevState, nextProps);
414 + } else {
415 + // Partial state object
416 + partialState = payload;
417 + }
418 + if (partialState === null || partialState === undefined) {
419 + // Null and undefined are treated as no-ops.
420 + return prevState;
421 + }
422 + // Merge the partial state and the previous state.
423 + return Object.assign({}, prevState, partialState);
424 + }
425 + case ForceUpdate: {
426 + queue.hasForceUpdate = true;
427 + return prevState;
428 + }
429 + }
430 + return prevState;
431 +}
432 +
433 +export function processUpdateQueue<State>(
434 + workInProgress: Fiber,
435 + queue: UpdateQueue<State>,
436 + props: any,
437 + instance: any,
438 + renderExpirationTime: ExpirationTime,
439 +): void {
440 + if (
441 + queue.expirationTime === NoWork ||
442 + queue.expirationTime > renderExpirationTime
443 + ) {
444 + // Insufficient priority. Bailout.
445 + return;
446 + }
447 +
448 + queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);
449 +
450 + if (__DEV__) {
451 + currentlyProcessingQueue = queue;
452 + }
453 +
454 + // These values may change as we process the queue.
455 + let newBaseState = queue.baseState;
456 + let newFirstUpdate = null;
457 + let newExpirationTime = NoWork;
458 +
459 + // Iterate through the list of updates to compute the result.
460 + let update = queue.firstUpdate;
461 + let resultState = newBaseState;
462 + while (update !== null) {
463 + const updateExpirationTime = update.expirationTime;
464 + if (updateExpirationTime > renderExpirationTime) {
465 + // This update does not have sufficient priority. Skip it.
466 + if (newFirstUpdate === null) {
467 + // This is the first skipped update. It will be the first update in
468 + // the new list.
469 + newFirstUpdate = update;
470 + // Since this is the first update that was skipped, the current result
471 + // is the new base state.
472 + newBaseState = resultState;
473 + }
474 + // Since this update will remain in the list, update the remaining
475 + // expiration time.
476 + if (
477 + newExpirationTime === NoWork ||
478 + newExpirationTime > updateExpirationTime
479 + ) {
480 + newExpirationTime = updateExpirationTime;
481 + }
482 + } else {
483 + // This update does have sufficient priority. Process it and compute
484 + // a new result.
485 + resultState = getStateFromUpdate(
486 + workInProgress,
487 + queue,
488 + update,
489 + resultState,
490 + props,
491 + instance,
492 + );
493 + const callback = update.callback;
494 + if (callback !== null) {
495 + workInProgress.effectTag |= Callback;
496 + // Set this to null, in case it was mutated during an aborted render.
497 + update.nextEffect = null;
498 + if (queue.lastEffect === null) {
499 + queue.firstEffect = queue.lastEffect = update;
500 + } else {
501 + queue.lastEffect.nextEffect = update;
502 + queue.lastEffect = update;
503 + }
504 + }
505 + }
506 + // Continue to the next update.
507 + update = update.next;
508 + }
509 +
510 + // Separately, iterate though the list of captured updates.
511 + let newFirstCapturedUpdate = null;
512 + update = queue.firstCapturedUpdate;
513 + while (update !== null) {
514 + const updateExpirationTime = update.expirationTime;
515 + if (updateExpirationTime > renderExpirationTime) {
516 + // This update does not have sufficient priority. Skip it.
517 + if (newFirstCapturedUpdate === null) {
518 + // This is the first skipped captured update. It will be the first
519 + // update in the new list.
520 + newFirstCapturedUpdate = update;
521 + // If this is the first update that was skipped, the current result is
522 + // the new base state.
523 + if (newFirstUpdate === null) {
524 + newBaseState = resultState;
525 + }
526 + }
527 + // Since this update will remain in the list, update the remaining
528 + // expiration time.
529 + if (
530 + newExpirationTime === NoWork ||
531 + newExpirationTime > updateExpirationTime
532 + ) {
533 + newExpirationTime = updateExpirationTime;
534 + }
535 + } else {
536 + // This update does have sufficient priority. Process it and compute
537 + // a new result.
538 + resultState = getStateFromUpdate(
539 + workInProgress,
540 + queue,
541 + update,
542 + resultState,
543 + props,
544 + instance,
545 + );
546 + const callback = update.callback;
547 + if (callback !== null) {
548 + workInProgress.effectTag |= Callback;
549 + // Set this to null, in case it was mutated during an aborted render.
550 + update.nextEffect = null;
551 + if (queue.lastCapturedEffect === null) {
552 + queue.firstCapturedEffect = queue.lastCapturedEffect = update;
553 + } else {
554 + queue.lastCapturedEffect.nextEffect = update;
555 + queue.lastCapturedEffect = update;
556 + }
557 + }
558 + }
559 + update = update.next;
560 + }
561 +
562 + if (newFirstUpdate === null) {
563 + queue.lastUpdate = null;
564 + }
565 + if (newFirstCapturedUpdate === null) {
566 + queue.lastCapturedUpdate = null;
567 + } else {
568 + workInProgress.effectTag |= Callback;
569 + }
570 + if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
571 + // We processed every update, without skipping. That means the new base
572 + // state is the same as the result state.
573 + newBaseState = resultState;
574 + }
575 +
576 + queue.baseState = newBaseState;
577 + queue.firstUpdate = newFirstUpdate;
578 + queue.firstCapturedUpdate = newFirstCapturedUpdate;
579 + queue.expirationTime = newExpirationTime;
580 +
581 + workInProgress.memoizedState = resultState;
582 +
583 + if (__DEV__) {
584 + currentlyProcessingQueue = null;
585 + }
586 +}
587 +
588 +function callCallback(callback, context) {
589 + invariant(
590 + typeof callback === 'function',
591 + 'Invalid argument passed as callback. Expected a function. Instead ' +
592 + 'received: %s',
593 + callback,
594 + );
595 + callback.call(context);
596 +}
597 +
598 +export function commitUpdateQueue<State>(
599 + finishedWork: Fiber,
600 + finishedQueue: UpdateQueue<State>,
601 + instance: any,
602 + renderExpirationTime: ExpirationTime,
603 +): void {
604 + // If the finished render included captured updates, and there are still
605 + // lower priority updates left over, we need to keep the captured updates
606 + // in the queue so that they are rebased and not dropped once we process the
607 + // queue again at the lower priority.
608 + if (finishedQueue.firstCapturedUpdate !== null) {
609 + // Join the captured update list to the end of the normal list.
610 + if (finishedQueue.lastUpdate !== null) {
611 + finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;
612 + finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;
613 + }
614 + // Clear the list of captured updates.
615 + finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;
616 + }
617 +
618 + // Commit the effects
619 + let effect = finishedQueue.firstEffect;
620 + finishedQueue.firstEffect = finishedQueue.lastEffect = null;
621 + while (effect !== null) {
622 + const callback = effect.callback;
623 + if (callback !== null) {
624 + effect.callback = null;
625 + callCallback(callback, instance);
626 + }
627 + effect = effect.nextEffect;
628 + }
629 +
630 + effect = finishedQueue.firstCapturedEffect;
631 + finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
632 + while (effect !== null) {
633 + const callback = effect.callback;
634 + if (callback !== null) {
635 + effect.callback = null;
636 + callCallback(callback, instance);
637 + }
638 + effect = effect.nextEffect;
639 + }
640 +}
packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
+8 -3
@@ -1003,6 +1003,7 @@ describe('ReactIncremental', () => {
1003 instance.setState(updater);
1004 ReactNoop.flush();
1005 expect(instance.state.num).toEqual(2);
1006 +
1007 instance.setState(updater);
1008 ReactNoop.render(<Foo multiplier={3} />);
1009 ReactNoop.flush();
@@ -1421,7 +1422,7 @@ describe('ReactIncremental', () => {
1422 ]);
1423 });
1424
1424 - it('does not call static getDerivedStateFromProps for state-only updates', () => {
1425 + it('calls getDerivedStateFromProps even for state-only updates', () => {
1426 let ops = [];
1427 let instance;
1428
@@ -1455,8 +1456,12 @@ describe('ReactIncremental', () => {
1456 instance.changeState();
1457 ReactNoop.flush();
1458
1458 - expect(ops).toEqual(['render', 'componentDidUpdate']);
1459 - expect(instance.state).toEqual({foo: 'bar'});
1459 + expect(ops).toEqual([
1460 + 'getDerivedStateFromProps',
1461 + 'render',
1462 + 'componentDidUpdate',
1463 + ]);
1464 + expect(instance.state).toEqual({foo: 'foo'});
1465 });
1466
1467 xit('does not call componentWillReceiveProps for state-only updates', () => {
packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js
+2
@@ -427,6 +427,8 @@ describe('ReactIncrementalTriangle', () => {
427
428 function simulate(...actions) {
429 const gen = simulateAndYield();
430 + // Call this once to prepare the generator
431 + gen.next();
432 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
433 for (let action of actions) {
434 gen.next(action);
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+4 -4
@@ -298,7 +298,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
298 ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update
299 ⚛ (Committing Snapshot Effects: 0 Total)
300 ⚛ (Committing Host Effects: 2 Total)
301 - ⚛ (Calling Lifecycle Methods: 0 Total)
301 + ⚛ (Calling Lifecycle Methods: 1 Total)
302
303 ⚛ (React Tree Reconciliation: Completed Root)
304 ⚛ Boundary [update]
@@ -324,7 +324,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
324 ⚛ (Committing Changes)
325 ⚛ (Committing Snapshot Effects: 0 Total)
326 ⚛ (Committing Host Effects: 1 Total)
327 - ⚛ (Calling Lifecycle Methods: 0 Total)
327 + ⚛ (Calling Lifecycle Methods: 1 Total)
328
329 ⚛ (Waiting for async callback... will force flush in 5230 ms)
330
@@ -406,8 +406,8 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
406
407 ⚛ (Committing Changes)
408 ⚛ (Committing Snapshot Effects: 0 Total)
409 - ⚛ (Committing Host Effects: 1 Total)
410 - ⚛ (Calling Lifecycle Methods: 1 Total)
409 + ⚛ (Committing Host Effects: 0 Total)
410 + ⚛ (Calling Lifecycle Methods: 0 Total)
411 "
412 `;
413
packages/react/src/__tests__/ReactStrictMode-test.internal.js
+54 -28
@@ -57,38 +57,64 @@ describe('ReactStrictMode', () => {
57
58 const component = ReactTestRenderer.create(<ClassComponent />);
59
60 - expect(log).toEqual([
61 - 'constructor',
62 - 'constructor',
63 - 'getDerivedStateFromProps',
64 - 'getDerivedStateFromProps',
65 - 'render',
66 - 'render',
67 - 'componentDidMount',
68 - ]);
60 + if (__DEV__) {
61 + expect(log).toEqual([
62 + 'constructor',
63 + 'constructor',
64 + 'getDerivedStateFromProps',
65 + 'getDerivedStateFromProps',
66 + 'render',
67 + 'render',
68 + 'componentDidMount',
69 + ]);
70 + } else {
71 + expect(log).toEqual([
72 + 'constructor',
73 + 'getDerivedStateFromProps',
74 + 'render',
75 + 'componentDidMount',
76 + ]);
77 + }
78
79 log = [];
80 shouldComponentUpdate = true;
81
82 component.update(<ClassComponent />);
74 - expect(log).toEqual([
75 - 'getDerivedStateFromProps',
76 - 'getDerivedStateFromProps',
77 - 'shouldComponentUpdate',
78 - 'render',
79 - 'render',
80 - 'componentDidUpdate',
81 - ]);
83 + if (__DEV__) {
84 + expect(log).toEqual([
85 + 'getDerivedStateFromProps',
86 + 'getDerivedStateFromProps',
87 + 'shouldComponentUpdate',
88 + 'render',
89 + 'render',
90 + 'componentDidUpdate',
91 + ]);
92 + } else {
93 + expect(log).toEqual([
94 + 'getDerivedStateFromProps',
95 + 'shouldComponentUpdate',
96 + 'render',
97 + 'componentDidUpdate',
98 + ]);
99 + }
100
101 log = [];
102 shouldComponentUpdate = false;
103
104 component.update(<ClassComponent />);
87 - expect(log).toEqual([
88 - 'getDerivedStateFromProps',
89 - 'getDerivedStateFromProps',
90 - 'shouldComponentUpdate',
91 - ]);
105 +
106 + if (__DEV__) {
107 + expect(log).toEqual([
108 + 'getDerivedStateFromProps',
109 + 'getDerivedStateFromProps',
110 + 'shouldComponentUpdate',
111 + ]);
112 + } else {
113 + expect(log).toEqual([
114 + 'getDerivedStateFromProps',
115 + 'shouldComponentUpdate',
116 + ]);
117 + }
118 });
119
120 it('should invoke setState callbacks twice', () => {
@@ -112,8 +138,8 @@ describe('ReactStrictMode', () => {
138 };
139 });
140
115 - // Callback should be invoked twice
116 - expect(setStateCount).toBe(2);
141 + // Callback should be invoked twice in DEV
142 + expect(setStateCount).toBe(__DEV__ ? 2 : 1);
143 // But each time `state` should be the previous value
144 expect(instance.state.count).toBe(2);
145 });
@@ -174,7 +200,7 @@ describe('ReactStrictMode', () => {
200
201 const component = ReactTestRenderer.create(<Root />);
202
177 - if (debugRenderPhaseSideEffectsForStrictMode) {
203 + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) {
204 expect(log).toEqual([
205 'constructor',
206 'constructor',
@@ -197,7 +223,7 @@ describe('ReactStrictMode', () => {
223 shouldComponentUpdate = true;
224
225 component.update(<Root />);
200 - if (debugRenderPhaseSideEffectsForStrictMode) {
226 + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) {
227 expect(log).toEqual([
228 'getDerivedStateFromProps',
229 'getDerivedStateFromProps',
@@ -219,7 +245,7 @@ describe('ReactStrictMode', () => {
245 shouldComponentUpdate = false;
246
247 component.update(<Root />);
222 - if (debugRenderPhaseSideEffectsForStrictMode) {
248 + if (__DEV__ && debugRenderPhaseSideEffectsForStrictMode) {
249 expect(log).toEqual([
250 'getDerivedStateFromProps',
251 'getDerivedStateFromProps',
@@ -263,7 +289,7 @@ describe('ReactStrictMode', () => {
289
290 // Callback should be invoked twice (in DEV)
291 expect(setStateCount).toBe(
266 - debugRenderPhaseSideEffectsForStrictMode ? 2 : 1,
292 + __DEV__ && debugRenderPhaseSideEffectsForStrictMode ? 2 : 1,
293 );
294 // But each time `state` should be the previous value
295 expect(instance.state.count).toBe(2);
packages/shared/ReactTypeOfSideEffect.js
+14 -15
@@ -10,23 +10,22 @@
10 export type TypeOfSideEffect = number;
11
12 // Don't change these two values. They're used by React Dev Tools.
13 -export const NoEffect = /* */ 0b000000000000;
14 -export const PerformedWork = /* */ 0b000000000001;
13 +export const NoEffect = /* */ 0b00000000000;
14 +export const PerformedWork = /* */ 0b00000000001;
15
16 // You can change the rest (and add more).
17 -export const Placement = /* */ 0b000000000010;
18 -export const Update = /* */ 0b000000000100;
19 -export const PlacementAndUpdate = /* */ 0b000000000110;
20 -export const Deletion = /* */ 0b000000001000;
21 -export const ContentReset = /* */ 0b000000010000;
22 -export const Callback = /* */ 0b000000100000;
23 -export const DidCapture = /* */ 0b000001000000;
24 -export const Ref = /* */ 0b000010000000;
25 -export const ErrLog = /* */ 0b000100000000;
26 -export const Snapshot = /* */ 0b100000000000;
17 +export const Placement = /* */ 0b00000000010;
18 +export const Update = /* */ 0b00000000100;
19 +export const PlacementAndUpdate = /* */ 0b00000000110;
20 +export const Deletion = /* */ 0b00000001000;
21 +export const ContentReset = /* */ 0b00000010000;
22 +export const Callback = /* */ 0b00000100000;
23 +export const DidCapture = /* */ 0b00001000000;
24 +export const Ref = /* */ 0b00010000000;
25 +export const Snapshot = /* */ 0b00100000000;
26
27 // Union of all host effects
29 -export const HostEffectMask = /* */ 0b100111111111;
28 +export const HostEffectMask = /* */ 0b00111111111;
29
31 -export const Incomplete = /* */ 0b001000000000;
32 -export const ShouldCapture = /* */ 0b010000000000;
30 +export const Incomplete = /* */ 0b01000000000;
31 +export const ShouldCapture = /* */ 0b10000000000;