@samitouri / QOS-React-2 / commits / 7bf40e1cfd

Initialize update queue object on mount (#17560)

* Refactor Update Queues to Fix Rebasing Bug Fixes a bug related to rebasing updates. Once an update has committed, it should never un-commit, even if interrupted by a higher priority update. The fix includes a refactor of how update queues work. This commit is a combination of two PRs: - #17483 by @sebmarkbage refactors the hook update queue - #17510 by @acdlite refactors the class and root update queue Landing one without the other would cause state updates to sometimes be inconsistent across components, so I've combined them into a single commit in case they need to be reverted. Co-authored-by: Sebastian Markbåge <sema@fb.com> Co-authored-by: Andrew Clark <git@andrewclark.io> * Initialize update queue object on mount Instead of lazily initializing update queue objects on the first update, class and host root queues are created on mount. This simplifies the logic for appending new updates and matches what we do for hooks.

Andrew Clark committed Dec 10, 2019 at 18:42 UTC 7bf40e1cfdb780788700a41bf30163fdb8d105a3
8 files changed +530 -442
packages/react-noop-renderer/src/createReactNoop.js
+24 -11
@@ -1142,20 +1142,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1142
1143 function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
1144 log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
1145 - const firstUpdate = updateQueue.firstUpdate;
1146 - if (!firstUpdate) {
1145 + const last = updateQueue.baseQueue;
1146 + if (last === null) {
1147 return;
1148 }
1149 + const first = last.next;
1150 + let update = first;
1151 + if (update !== null) {
1152 + do {
1153 + log(
1154 + ' '.repeat(depth + 1) + '~',
1155 + '[' + update.expirationTime + ']',
1156 + );
1157 + } while (update !== null && update !== first);
1158 + }
1159
1150 - log(
1151 - ' '.repeat(depth + 1) + '~',
1152 - '[' + firstUpdate.expirationTime + ']',
1153 - );
1154 - while (firstUpdate.next) {
1155 - log(
1156 - ' '.repeat(depth + 1) + '~',
1157 - '[' + firstUpdate.expirationTime + ']',
1158 - );
1160 + const lastPending = updateQueue.shared.pending;
1161 + if (lastPending !== null) {
1162 + const firstPending = lastPending.next;
1163 + let pendingUpdate = firstPending;
1164 + if (pendingUpdate !== null) {
1165 + do {
1166 + log(
1167 + ' '.repeat(depth + 1) + '~',
1168 + '[' + pendingUpdate.expirationTime + ']',
1169 + );
1170 + } while (pendingUpdate !== null && pendingUpdate !== firstPending);
1171 + }
1172 }
1173 }
1174
packages/react-reconciler/src/ReactFiberBeginWork.js
+10 -9
@@ -90,7 +90,11 @@ import {
90 reconcileChildFibers,
91 cloneChildFibers,
92 } from './ReactChildFiber';
93 -import {processUpdateQueue} from './ReactUpdateQueue';
93 +import {
94 + processUpdateQueue,
95 + cloneUpdateQueue,
96 + initializeUpdateQueue,
97 +} from './ReactUpdateQueue';
98 import {
99 NoWork,
100 Never,
@@ -904,7 +908,7 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
908 pushHostRootContext(workInProgress);
909 const updateQueue = workInProgress.updateQueue;
910 invariant(
907 - updateQueue !== null,
911 + current !== null && updateQueue !== null,
912 'If the root does not have an updateQueue, we should have already ' +
913 'bailed out. This error is likely caused by a bug in React. Please ' +
914 'file an issue.',
@@ -912,13 +916,8 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
916 const nextProps = workInProgress.pendingProps;
917 const prevState = workInProgress.memoizedState;
918 const prevChildren = prevState !== null ? prevState.element : null;
915 - processUpdateQueue(
916 - workInProgress,
917 - updateQueue,
918 - nextProps,
919 - null,
920 - renderExpirationTime,
921 - );
919 + cloneUpdateQueue(current, workInProgress);
920 + processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime);
921 const nextState = workInProgress.memoizedState;
922 // Caution: React DevTools currently depends on this property
923 // being called "element".
@@ -1338,6 +1337,8 @@ function mountIndeterminateComponent(
1337 workInProgress.memoizedState =
1338 value.state !== null && value.state !== undefined ? value.state : null;
1339
1340 + initializeUpdateQueue(workInProgress);
1341 +
1342 const getDerivedStateFromProps = Component.getDerivedStateFromProps;
1343 if (typeof getDerivedStateFromProps === 'function') {
1344 applyDerivedStateFromProps(
packages/react-reconciler/src/ReactFiberClassComponent.js
+23 -46
@@ -9,6 +9,7 @@
9
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 +import type {UpdateQueue} from './ReactUpdateQueue';
13
14 import React from 'react';
15 import {Update, Snapshot} from 'shared/ReactSideEffectTags';
@@ -38,6 +39,8 @@ import {
39 createUpdate,
40 ReplaceState,
41 ForceUpdate,
42 + initializeUpdateQueue,
43 + cloneUpdateQueue,
44 } from './ReactUpdateQueue';
45 import {NoWork} from './ReactFiberExpirationTime';
46 import {
@@ -171,8 +174,9 @@ export function applyDerivedStateFromProps(
174
175 // Once the update queue is empty, persist the derived state onto the
176 // base state.
174 - const updateQueue = workInProgress.updateQueue;
175 - if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
177 + if (workInProgress.expirationTime === NoWork) {
178 + // Queue is always non-null for classes
179 + const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
180 updateQueue.baseState = memoizedState;
181 }
182 }
@@ -789,6 +793,8 @@ function mountClassInstance(
793 instance.state = workInProgress.memoizedState;
794 instance.refs = emptyRefsObject;
795
796 + initializeUpdateQueue(workInProgress);
797 +
798 const contextType = ctor.contextType;
799 if (typeof contextType === 'object' && contextType !== null) {
800 instance.context = readContext(contextType);
@@ -829,17 +835,8 @@ function mountClassInstance(
835 }
836 }
837
832 - let updateQueue = workInProgress.updateQueue;
833 - if (updateQueue !== null) {
834 - processUpdateQueue(
835 - workInProgress,
836 - updateQueue,
837 - newProps,
838 - instance,
839 - renderExpirationTime,
840 - );
841 - instance.state = workInProgress.memoizedState;
842 - }
838 + processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
839 + instance.state = workInProgress.memoizedState;
840
841 const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
842 if (typeof getDerivedStateFromProps === 'function') {
@@ -863,17 +860,13 @@ function mountClassInstance(
860 callComponentWillMount(workInProgress, instance);
861 // If we had additional state updates during this life-cycle, let's
862 // process them now.
866 - updateQueue = workInProgress.updateQueue;
867 - if (updateQueue !== null) {
868 - processUpdateQueue(
869 - workInProgress,
870 - updateQueue,
871 - newProps,
872 - instance,
873 - renderExpirationTime,
874 - );
875 - instance.state = workInProgress.memoizedState;
876 - }
863 + processUpdateQueue(
864 + workInProgress,
865 + newProps,
866 + instance,
867 + renderExpirationTime,
868 + );
869 + instance.state = workInProgress.memoizedState;
870 }
871
872 if (typeof instance.componentDidMount === 'function') {
@@ -936,17 +929,8 @@ function resumeMountClassInstance(
929
930 const oldState = workInProgress.memoizedState;
931 let newState = (instance.state = oldState);
939 - let updateQueue = workInProgress.updateQueue;
940 - if (updateQueue !== null) {
941 - processUpdateQueue(
942 - workInProgress,
943 - updateQueue,
944 - newProps,
945 - instance,
946 - renderExpirationTime,
947 - );
948 - newState = workInProgress.memoizedState;
949 - }
932 + processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
933 + newState = workInProgress.memoizedState;
934 if (
935 oldProps === newProps &&
936 oldState === newState &&
@@ -1035,6 +1019,8 @@ function updateClassInstance(
1019 ): boolean {
1020 const instance = workInProgress.stateNode;
1021
1022 + cloneUpdateQueue(current, workInProgress);
1023 +
1024 const oldProps = workInProgress.memoizedProps;
1025 instance.props =
1026 workInProgress.type === workInProgress.elementType
@@ -1081,17 +1067,8 @@ function updateClassInstance(
1067
1068 const oldState = workInProgress.memoizedState;
1069 let newState = (instance.state = oldState);
1084 - let updateQueue = workInProgress.updateQueue;
1085 - if (updateQueue !== null) {
1086 - processUpdateQueue(
1087 - workInProgress,
1088 - updateQueue,
1089 - newProps,
1090 - instance,
1091 - renderExpirationTime,
1092 - );
1093 - newState = workInProgress.memoizedState;
1094 - }
1070 + processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
1071 + newState = workInProgress.memoizedState;
1072
1073 if (
1074 oldProps === newProps &&
packages/react-reconciler/src/ReactFiberHooks.js
+70 -49
@@ -20,7 +20,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
20
21 import ReactSharedInternals from 'shared/ReactSharedInternals';
22
23 -import {NoWork} from './ReactFiberExpirationTime';
23 +import {NoWork, Sync} from './ReactFiberExpirationTime';
24 import {readContext} from './ReactFiberNewContext';
25 import {createResponderListener} from './ReactFiberEvents';
26 import {
@@ -108,13 +108,13 @@ type Update<S, A> = {
108 action: A,
109 eagerReducer: ((S, A) => S) | null,
110 eagerState: S | null,
111 - next: Update<S, A> | null,
111 + next: Update<S, A>,
112
113 priority?: ReactPriorityLevel,
114 };
115
116 type UpdateQueue<S, A> = {
117 - last: Update<S, A> | null,
117 + pending: Update<S, A> | null,
118 dispatch: (A => mixed) | null,
119 lastRenderedReducer: ((S, A) => S) | null,
120 lastRenderedState: S | null,
@@ -144,7 +144,7 @@ export type Hook = {
144 memoizedState: any,
145
146 baseState: any,
147 - baseUpdate: Update<any, any> | null,
147 + baseQueue: Update<any, any> | null,
148 queue: UpdateQueue<any, any> | null,
149
150 next: Hook | null,
@@ -544,8 +544,8 @@ function mountWorkInProgressHook(): Hook {
544 memoizedState: null,
545
546 baseState: null,
547 + baseQueue: null,
548 queue: null,
548 - baseUpdate: null,
549
550 next: null,
551 };
@@ -604,8 +604,8 @@ function updateWorkInProgressHook(): Hook {
604 memoizedState: currentHook.memoizedState,
605
606 baseState: currentHook.baseState,
607 + baseQueue: currentHook.baseQueue,
608 queue: currentHook.queue,
608 - baseUpdate: currentHook.baseUpdate,
609
610 next: null,
611 };
@@ -645,7 +645,7 @@ function mountReducer<S, I, A>(
645 }
646 hook.memoizedState = hook.baseState = initialState;
647 const queue = (hook.queue = {
648 - last: null,
648 + pending: null,
649 dispatch: null,
650 lastRenderedReducer: reducer,
651 lastRenderedState: (initialState: any),
@@ -703,7 +703,7 @@ function updateReducer<S, I, A>(
703 // the base state unless the queue is empty.
704 // TODO: Not sure if this is the desired semantics, but it's what we
705 // do for gDSFP. I can't remember why.
706 - if (hook.baseUpdate === queue.last) {
706 + if (hook.baseQueue === null) {
707 hook.baseState = newState;
708 }
709
@@ -715,42 +715,55 @@ function updateReducer<S, I, A>(
715 return [hook.memoizedState, dispatch];
716 }
717
718 - // The last update in the entire queue
719 - const last = queue.last;
720 - // The last update that is part of the base state.
721 - const baseUpdate = hook.baseUpdate;
722 - const baseState = hook.baseState;
723 -
724 - // Find the first unprocessed update.
725 - let first;
726 - if (baseUpdate !== null) {
727 - if (last !== null) {
728 - // For the first update, the queue is a circular linked list where
729 - // `queue.last.next = queue.first`. Once the first update commits, and
730 - // the `baseUpdate` is no longer empty, we can unravel the list.
731 - last.next = null;
718 + const current: Hook = (currentHook: any);
719 +
720 + // The last rebase update that is NOT part of the base state.
721 + let baseQueue = current.baseQueue;
722 +
723 + // The last pending update that hasn't been processed yet.
724 + let pendingQueue = queue.pending;
725 + if (pendingQueue !== null) {
726 + // We have new updates that haven't been processed yet.
727 + // We'll add them to the base queue.
728 + if (baseQueue !== null) {
729 + // Merge the pending queue and the base queue.
730 + let baseFirst = baseQueue.next;
731 + let pendingFirst = pendingQueue.next;
732 + baseQueue.next = pendingFirst;
733 + pendingQueue.next = baseFirst;
734 }
733 - first = baseUpdate.next;
734 - } else {
735 - first = last !== null ? last.next : null;
735 + current.baseQueue = baseQueue = pendingQueue;
736 + queue.pending = null;
737 }
737 - if (first !== null) {
738 - let newState = baseState;
738 +
739 + if (baseQueue !== null) {
740 + // We have a queue to process.
741 + let first = baseQueue.next;
742 + let newState = current.baseState;
743 +
744 let newBaseState = null;
740 - let newBaseUpdate = null;
741 - let prevUpdate = baseUpdate;
745 + let newBaseQueueFirst = null;
746 + let newBaseQueueLast = null;
747 let update = first;
743 - let didSkip = false;
748 do {
749 const updateExpirationTime = update.expirationTime;
750 if (updateExpirationTime < renderExpirationTime) {
751 // Priority is insufficient. Skip this update. If this is the first
752 // skipped update, the previous update/state is the new base
753 // update/state.
750 - if (!didSkip) {
751 - didSkip = true;
752 - newBaseUpdate = prevUpdate;
754 + const clone: Update<S, A> = {
755 + expirationTime: update.expirationTime,
756 + suspenseConfig: update.suspenseConfig,
757 + action: update.action,
758 + eagerReducer: update.eagerReducer,
759 + eagerState: update.eagerState,
760 + next: (null: any),
761 + };
762 + if (newBaseQueueLast === null) {
763 + newBaseQueueFirst = newBaseQueueLast = clone;
764 newBaseState = newState;
765 + } else {
766 + newBaseQueueLast = newBaseQueueLast.next = clone;
767 }
768 // Update the remaining priority in the queue.
769 if (updateExpirationTime > currentlyRenderingFiber.expirationTime) {
@@ -760,6 +773,18 @@ function updateReducer<S, I, A>(
773 } else {
774 // This update does have sufficient priority.
775
776 + if (newBaseQueueLast !== null) {
777 + const clone: Update<S, A> = {
778 + expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
779 + suspenseConfig: update.suspenseConfig,
780 + action: update.action,
781 + eagerReducer: update.eagerReducer,
782 + eagerState: update.eagerState,
783 + next: (null: any),
784 + };
785 + newBaseQueueLast = newBaseQueueLast.next = clone;
786 + }
787 +
788 // Mark the event time of this update as relevant to this render pass.
789 // TODO: This should ideally use the true event time of this update rather than
790 // its priority which is a derived and not reverseable value.
@@ -781,13 +806,13 @@ function updateReducer<S, I, A>(
806 newState = reducer(newState, action);
807 }
808 }
784 - prevUpdate = update;
809 update = update.next;
810 } while (update !== null && update !== first);
811
788 - if (!didSkip) {
789 - newBaseUpdate = prevUpdate;
812 + if (newBaseQueueLast === null) {
813 newBaseState = newState;
814 + } else {
815 + newBaseQueueLast.next = (newBaseQueueFirst: any);
816 }
817
818 // Mark that the fiber performed work, but only if the new state is
@@ -797,8 +822,8 @@ function updateReducer<S, I, A>(
822 }
823
824 hook.memoizedState = newState;
800 - hook.baseUpdate = newBaseUpdate;
825 hook.baseState = newBaseState;
826 + hook.baseQueue = newBaseQueueLast;
827
828 queue.lastRenderedState = newState;
829 }
@@ -816,7 +841,7 @@ function mountState<S>(
841 }
842 hook.memoizedState = hook.baseState = initialState;
843 const queue = (hook.queue = {
819 - last: null,
844 + pending: null,
845 dispatch: null,
846 lastRenderedReducer: basicStateReducer,
847 lastRenderedState: (initialState: any),
@@ -1233,7 +1258,7 @@ function dispatchAction<S, A>(
1258 action,
1259 eagerReducer: null,
1260 eagerState: null,
1236 - next: null,
1261 + next: (null: any),
1262 };
1263 if (__DEV__) {
1264 update.priority = getCurrentPriorityLevel();
@@ -1267,7 +1292,7 @@ function dispatchAction<S, A>(
1292 action,
1293 eagerReducer: null,
1294 eagerState: null,
1270 - next: null,
1295 + next: (null: any),
1296 };
1297
1298 if (__DEV__) {
@@ -1275,19 +1300,15 @@ function dispatchAction<S, A>(
1300 }
1301
1302 // Append the update to the end of the list.
1278 - const last = queue.last;
1279 - if (last === null) {
1303 + const pending = queue.pending;
1304 + if (pending === null) {
1305 // This is the first update. Create a circular list.
1306 update.next = update;
1307 } else {
1283 - const first = last.next;
1284 - if (first !== null) {
1285 - // Still circular.
1286 - update.next = first;
1287 - }
1288 - last.next = update;
1308 + update.next = pending.next;
1309 + pending.next = update;
1310 }
1290 - queue.last = update;
1311 + queue.pending = update;
1312
1313 if (
1314 fiber.expirationTime === NoWork &&
packages/react-reconciler/src/ReactFiberRoot.js
+3
@@ -25,6 +25,7 @@ import {
25 } from 'shared/ReactFeatureFlags';
26 import {unstable_getThreadID} from 'scheduler/tracing';
27 import {NoPriority} from './SchedulerWithReactIntegration';
28 +import {initializeUpdateQueue} from './ReactUpdateQueue';
29
30 export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
31
@@ -149,6 +150,8 @@ export function createFiberRoot(
150 root.current = uninitializedFiber;
151 uninitializedFiber.stateNode = root;
152
153 + initializeUpdateQueue(uninitializedFiber);
154 +
155 return root;
156 }
157
packages/react-reconciler/src/ReactFiberWorkLoop.js
+9 -10
@@ -14,6 +14,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
14 import type {Interaction} from 'scheduler/src/Tracing';
15 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16 import type {SuspenseState} from './ReactFiberSuspenseComponent';
17 +import type {Hook} from './ReactFiberHooks';
18
19 import {
20 warnAboutDeprecatedLifecycles,
@@ -2859,7 +2860,7 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
2860 // has triggered any high priority updates
2861 const updateQueue = current.updateQueue;
2862 if (updateQueue !== null) {
2862 - let update = updateQueue.firstUpdate;
2863 + let update = updateQueue.baseQueue;
2864 while (update !== null) {
2865 const priorityLevel = update.priority;
2866 if (
@@ -2883,12 +2884,11 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
2884 break;
2885 case FunctionComponent:
2886 case ForwardRef:
2886 - case SimpleMemoComponent:
2887 - if (
2888 - workInProgressNode.memoizedState !== null &&
2889 - workInProgressNode.memoizedState.baseUpdate !== null
2890 - ) {
2891 - let update = workInProgressNode.memoizedState.baseUpdate;
2887 + case SimpleMemoComponent: {
2888 + let firstHook: null | Hook = current.memoizedState;
2889 + // TODO: This just checks the first Hook. Isn't it suppose to check all Hooks?
2890 + if (firstHook !== null && firstHook.baseQueue !== null) {
2891 + let update = firstHook.baseQueue;
2892 // Loop through the functional component's memoized state to see whether
2893 // the component has triggered any high pri updates
2894 while (update !== null) {
@@ -2908,15 +2908,14 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
2908 }
2909 break;
2910 }
2911 - if (
2912 - update.next === workInProgressNode.memoizedState.baseUpdate
2913 - ) {
2911 + if (update.next === firstHook.baseQueue) {
2912 break;
2913 }
2914 update = update.next;
2915 }
2916 }
2917 break;
2918 + }
2919 default:
2920 break;
2921 }
packages/react-reconciler/src/ReactUpdateQueue.js
+219 -317
@@ -89,13 +89,12 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
89 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
90 import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
91
92 -import {NoWork} from './ReactFiberExpirationTime';
92 +import {NoWork, Sync} from './ReactFiberExpirationTime';
93 import {
94 enterDisallowedContextReadInDEV,
95 exitDisallowedContextReadInDEV,
96 } from './ReactFiberNewContext';
97 import {Callback, ShouldCapture, DidCapture} from 'shared/ReactSideEffectTags';
98 -import {ClassComponent} from 'shared/ReactWorkTags';
98
99 import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';
100
@@ -117,27 +116,21 @@ export type Update<State> = {
116 payload: any,
117 callback: (() => mixed) | null,
118
120 - next: Update<State> | null,
121 - nextEffect: Update<State> | null,
119 + next: Update<State>,
120
121 //DEV only
122 priority?: ReactPriorityLevel,
123 };
124
125 +type SharedQueue<State> = {
126 + pending: Update<State> | null,
127 +};
128 +
129 export type UpdateQueue<State> = {
130 baseState: State,
129 -
130 - firstUpdate: Update<State> | null,
131 - lastUpdate: Update<State> | null,
132 -
133 - firstCapturedUpdate: Update<State> | null,
134 - lastCapturedUpdate: Update<State> | null,
135 -
136 - firstEffect: Update<State> | null,
137 - lastEffect: Update<State> | null,
138 -
139 - firstCapturedEffect: Update<State> | null,
140 - lastCapturedEffect: Update<State> | null,
131 + baseQueue: Update<State> | null,
132 + shared: SharedQueue<State>,
133 + effects: Array<Update<State>> | null,
134 };
135
136 export const UpdateState = 0;
@@ -161,41 +154,34 @@ if (__DEV__) {
154 };
155 }
156
164 -export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
157 +export function initializeUpdateQueue<State>(fiber: Fiber): void {
158 const queue: UpdateQueue<State> = {
166 - baseState,
167 - firstUpdate: null,
168 - lastUpdate: null,
169 - firstCapturedUpdate: null,
170 - lastCapturedUpdate: null,
171 - firstEffect: null,
172 - lastEffect: null,
173 - firstCapturedEffect: null,
174 - lastCapturedEffect: null,
159 + baseState: fiber.memoizedState,
160 + baseQueue: null,
161 + shared: {
162 + pending: null,
163 + },
164 + effects: null,
165 };
176 - return queue;
166 + fiber.updateQueue = queue;
167 }
168
179 -function cloneUpdateQueue<State>(
180 - currentQueue: UpdateQueue<State>,
181 -): UpdateQueue<State> {
182 - const queue: UpdateQueue<State> = {
183 - baseState: currentQueue.baseState,
184 - firstUpdate: currentQueue.firstUpdate,
185 - lastUpdate: currentQueue.lastUpdate,
186 -
187 - // TODO: With resuming, if we bail out and resuse the child tree, we should
188 - // keep these effects.
189 - firstCapturedUpdate: null,
190 - lastCapturedUpdate: null,
191 -
192 - firstEffect: null,
193 - lastEffect: null,
194 -
195 - firstCapturedEffect: null,
196 - lastCapturedEffect: null,
197 - };
198 - return queue;
169 +export function cloneUpdateQueue<State>(
170 + current: Fiber,
171 + workInProgress: Fiber,
172 +): void {
173 + // Clone the update queue from current. Unless it's already a clone.
174 + const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
175 + const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
176 + if (queue === currentQueue) {
177 + const clone: UpdateQueue<State> = {
178 + baseState: currentQueue.baseState,
179 + baseQueue: currentQueue.baseQueue,
180 + shared: currentQueue.shared,
181 + effects: currentQueue.effects,
182 + };
183 + workInProgress.updateQueue = clone;
184 + }
185 }
186
187 export function createUpdate(
@@ -210,90 +196,36 @@ export function createUpdate(
196 payload: null,
197 callback: null,
198
213 - next: null,
214 - nextEffect: null,
199 + next: (null: any),
200 };
201 + update.next = update;
202 if (__DEV__) {
203 update.priority = getCurrentPriorityLevel();
204 }
205 return update;
206 }
207
222 -function appendUpdateToQueue<State>(
223 - queue: UpdateQueue<State>,
224 - update: Update<State>,
225 -) {
226 - // Append the update to the end of the list.
227 - if (queue.lastUpdate === null) {
228 - // Queue is empty
229 - queue.firstUpdate = queue.lastUpdate = update;
230 - } else {
231 - queue.lastUpdate.next = update;
232 - queue.lastUpdate = update;
233 - }
234 -}
235 -
208 export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
237 - // Update queues are created lazily.
238 - const alternate = fiber.alternate;
239 - let queue1;
240 - let queue2;
241 - if (alternate === null) {
242 - // There's only one fiber.
243 - queue1 = fiber.updateQueue;
244 - queue2 = null;
245 - if (queue1 === null) {
246 - queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
247 - }
248 - } else {
249 - // There are two owners.
250 - queue1 = fiber.updateQueue;
251 - queue2 = alternate.updateQueue;
252 - if (queue1 === null) {
253 - if (queue2 === null) {
254 - // Neither fiber has an update queue. Create new ones.
255 - queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
256 - queue2 = alternate.updateQueue = createUpdateQueue(
257 - alternate.memoizedState,
258 - );
259 - } else {
260 - // Only one fiber has an update queue. Clone to create a new one.
261 - queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
262 - }
263 - } else {
264 - if (queue2 === null) {
265 - // Only one fiber has an update queue. Clone to create a new one.
266 - queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
267 - } else {
268 - // Both owners have an update queue.
269 - }
270 - }
209 + const updateQueue = fiber.updateQueue;
210 + if (updateQueue === null) {
211 + // Only occurs if the fiber has been unmounted.
212 + return;
213 }
272 - if (queue2 === null || queue1 === queue2) {
273 - // There's only a single queue.
274 - appendUpdateToQueue(queue1, update);
214 +
215 + const sharedQueue = updateQueue.shared;
216 + const pending = sharedQueue.pending;
217 + if (pending === null) {
218 + // This is the first update. Create a circular list.
219 + update.next = update;
220 } else {
276 - // There are two queues. We need to append the update to both queues,
277 - // while accounting for the persistent structure of the list — we don't
278 - // want the same update to be added multiple times.
279 - if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
280 - // One of the queues is not empty. We must add the update to both queues.
281 - appendUpdateToQueue(queue1, update);
282 - appendUpdateToQueue(queue2, update);
283 - } else {
284 - // Both queues are non-empty. The last update is the same in both lists,
285 - // because of structural sharing. So, only append to one of the lists.
286 - appendUpdateToQueue(queue1, update);
287 - // But we still need to update the `lastUpdate` pointer of queue2.
288 - queue2.lastUpdate = update;
289 - }
221 + update.next = pending.next;
222 + pending.next = update;
223 }
224 + sharedQueue.pending = update;
225
226 if (__DEV__) {
227 if (
294 - fiber.tag === ClassComponent &&
295 - (currentlyProcessingQueue === queue1 ||
296 - (queue2 !== null && currentlyProcessingQueue === queue2)) &&
228 + currentlyProcessingQueue === sharedQueue &&
229 !didWarnUpdateInsideUpdate
230 ) {
231 warningWithoutStack(
@@ -312,48 +244,25 @@ export function enqueueCapturedUpdate<State>(
244 workInProgress: Fiber,
245 update: Update<State>,
246 ) {
315 - // Captured updates go into a separate list, and only on the work-in-
316 - // progress queue.
317 - let workInProgressQueue = workInProgress.updateQueue;
318 - if (workInProgressQueue === null) {
319 - workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
320 - workInProgress.memoizedState,
321 - );
322 - } else {
323 - // TODO: I put this here rather than createWorkInProgress so that we don't
324 - // clone the queue unnecessarily. There's probably a better way to
325 - // structure this.
326 - workInProgressQueue = ensureWorkInProgressQueueIsAClone(
327 - workInProgress,
328 - workInProgressQueue,
329 - );
247 + const current = workInProgress.alternate;
248 + if (current !== null) {
249 + // Ensure the work-in-progress queue is a clone
250 + cloneUpdateQueue(current, workInProgress);
251 }
252
253 + // Captured updates go only on the work-in-progress queue.
254 + const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
255 // Append the update to the end of the list.
333 - if (workInProgressQueue.lastCapturedUpdate === null) {
334 - // This is the first render phase update
335 - workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
256 + const last = queue.baseQueue;
257 + if (last === null) {
258 + queue.baseQueue = update.next = update;
259 + update.next = update;
260 } else {
337 - workInProgressQueue.lastCapturedUpdate.next = update;
338 - workInProgressQueue.lastCapturedUpdate = update;
261 + update.next = last.next;
262 + last.next = update;
263 }
264 }
265
342 -function ensureWorkInProgressQueueIsAClone<State>(
343 - workInProgress: Fiber,
344 - queue: UpdateQueue<State>,
345 -): UpdateQueue<State> {
346 - const current = workInProgress.alternate;
347 - if (current !== null) {
348 - // If the work-in-progress queue is equal to the current queue,
349 - // we need to clone it first.
350 - if (queue === current.updateQueue) {
351 - queue = workInProgress.updateQueue = cloneUpdateQueue(queue);
352 - }
353 - }
354 - return queue;
355 -}
356 -
266 function getStateFromUpdate<State>(
267 workInProgress: Fiber,
268 queue: UpdateQueue<State>,
@@ -429,158 +338,172 @@ function getStateFromUpdate<State>(
338
339 export function processUpdateQueue<State>(
340 workInProgress: Fiber,
432 - queue: UpdateQueue<State>,
341 props: any,
342 instance: any,
343 renderExpirationTime: ExpirationTime,
344 ): void {
437 - hasForceUpdate = false;
345 + // This is always non-null on a ClassComponent or HostRoot
346 + const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
347
439 - queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);
348 + hasForceUpdate = false;
349
350 if (__DEV__) {
442 - currentlyProcessingQueue = queue;
351 + currentlyProcessingQueue = queue.shared;
352 }
353
445 - // These values may change as we process the queue.
446 - let newBaseState = queue.baseState;
447 - let newFirstUpdate = null;
448 - let newExpirationTime = NoWork;
449 -
450 - // Iterate through the list of updates to compute the result.
451 - let update = queue.firstUpdate;
452 - let resultState = newBaseState;
453 - while (update !== null) {
454 - const updateExpirationTime = update.expirationTime;
455 - if (updateExpirationTime < renderExpirationTime) {
456 - // This update does not have sufficient priority. Skip it.
457 - if (newFirstUpdate === null) {
458 - // This is the first skipped update. It will be the first update in
459 - // the new list.
460 - newFirstUpdate = update;
461 - // Since this is the first update that was skipped, the current result
462 - // is the new base state.
463 - newBaseState = resultState;
464 - }
465 - // Since this update will remain in the list, update the remaining
466 - // expiration time.
467 - if (newExpirationTime < updateExpirationTime) {
468 - newExpirationTime = updateExpirationTime;
469 - }
470 - } else {
471 - // This update does have sufficient priority.
472 -
473 - // Mark the event time of this update as relevant to this render pass.
474 - // TODO: This should ideally use the true event time of this update rather than
475 - // its priority which is a derived and not reverseable value.
476 - // TODO: We should skip this update if it was already committed but currently
477 - // we have no way of detecting the difference between a committed and suspended
478 - // update here.
479 - markRenderEventTimeAndConfig(updateExpirationTime, update.suspenseConfig);
480 -
481 - // Process it and compute a new result.
482 - resultState = getStateFromUpdate(
483 - workInProgress,
484 - queue,
485 - update,
486 - resultState,
487 - props,
488 - instance,
489 - );
490 - const callback = update.callback;
491 - if (callback !== null) {
492 - workInProgress.effectTag |= Callback;
493 - // Set this to null, in case it was mutated during an aborted render.
494 - update.nextEffect = null;
495 - if (queue.lastEffect === null) {
496 - queue.firstEffect = queue.lastEffect = update;
497 - } else {
498 - queue.lastEffect.nextEffect = update;
499 - queue.lastEffect = update;
500 - }
354 + // The last rebase update that is NOT part of the base state.
355 + let baseQueue = queue.baseQueue;
356 +
357 + // The last pending update that hasn't been processed yet.
358 + let pendingQueue = queue.shared.pending;
359 + if (pendingQueue !== null) {
360 + // We have new updates that haven't been processed yet.
361 + // We'll add them to the base queue.
362 + if (baseQueue !== null) {
363 + // Merge the pending queue and the base queue.
364 + let baseFirst = baseQueue.next;
365 + let pendingFirst = pendingQueue.next;
366 + baseQueue.next = pendingFirst;
367 + pendingQueue.next = baseFirst;
368 + }
369 +
370 + baseQueue = pendingQueue;
371 +
372 + queue.shared.pending = null;
373 + // TODO: Pass `current` as argument
374 + const current = workInProgress.alternate;
375 + if (current !== null) {
376 + const currentQueue = current.updateQueue;
377 + if (currentQueue !== null) {
378 + currentQueue.baseQueue = pendingQueue;
379 }
380 }
503 - // Continue to the next update.
504 - update = update.next;
381 }
382
507 - // Separately, iterate though the list of captured updates.
508 - let newFirstCapturedUpdate = null;
509 - update = queue.firstCapturedUpdate;
510 - while (update !== null) {
511 - const updateExpirationTime = update.expirationTime;
512 - if (updateExpirationTime < renderExpirationTime) {
513 - // This update does not have sufficient priority. Skip it.
514 - if (newFirstCapturedUpdate === null) {
515 - // This is the first skipped captured update. It will be the first
516 - // update in the new list.
517 - newFirstCapturedUpdate = update;
518 - // If this is the first update that was skipped, the current result is
519 - // the new base state.
520 - if (newFirstUpdate === null) {
521 - newBaseState = resultState;
522 - }
523 - }
524 - // Since this update will remain in the list, update the remaining
525 - // expiration time.
526 - if (newExpirationTime < updateExpirationTime) {
527 - newExpirationTime = updateExpirationTime;
528 - }
529 - } else {
530 - // This update does have sufficient priority. Process it and compute
531 - // a new result.
532 - resultState = getStateFromUpdate(
533 - workInProgress,
534 - queue,
535 - update,
536 - resultState,
537 - props,
538 - instance,
539 - );
540 - const callback = update.callback;
541 - if (callback !== null) {
542 - workInProgress.effectTag |= Callback;
543 - // Set this to null, in case it was mutated during an aborted render.
544 - update.nextEffect = null;
545 - if (queue.lastCapturedEffect === null) {
546 - queue.firstCapturedEffect = queue.lastCapturedEffect = update;
383 + // These values may change as we process the queue.
384 + if (baseQueue !== null) {
385 + let first = baseQueue.next;
386 + // Iterate through the list of updates to compute the result.
387 + let newState = queue.baseState;
388 + let newExpirationTime = NoWork;
389 +
390 + let newBaseState = null;
391 + let newBaseQueueFirst = null;
392 + let newBaseQueueLast = null;
393 +
394 + if (first !== null) {
395 + let update = first;
396 + do {
397 + const updateExpirationTime = update.expirationTime;
398 + if (updateExpirationTime < renderExpirationTime) {
399 + // Priority is insufficient. Skip this update. If this is the first
400 + // skipped update, the previous update/state is the new base
401 + // update/state.
402 + const clone: Update<State> = {
403 + expirationTime: update.expirationTime,
404 + suspenseConfig: update.suspenseConfig,
405 +
406 + tag: update.tag,
407 + payload: update.payload,
408 + callback: update.callback,
409 +
410 + next: (null: any),
411 + };
412 + if (newBaseQueueLast === null) {
413 + newBaseQueueFirst = newBaseQueueLast = clone;
414 + newBaseState = newState;
415 + } else {
416 + newBaseQueueLast = newBaseQueueLast.next = clone;
417 + }
418 + // Update the remaining priority in the queue.
419 + if (updateExpirationTime > newExpirationTime) {
420 + newExpirationTime = updateExpirationTime;
421 + }
422 } else {
548 - queue.lastCapturedEffect.nextEffect = update;
549 - queue.lastCapturedEffect = update;
423 + // This update does have sufficient priority.
424 +
425 + if (newBaseQueueLast !== null) {
426 + const clone: Update<State> = {
427 + expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
428 + suspenseConfig: update.suspenseConfig,
429 +
430 + tag: update.tag,
431 + payload: update.payload,
432 + callback: update.callback,
433 +
434 + next: (null: any),
435 + };
436 + newBaseQueueLast = newBaseQueueLast.next = clone;
437 + }
438 +
439 + // Mark the event time of this update as relevant to this render pass.
440 + // TODO: This should ideally use the true event time of this update rather than
441 + // its priority which is a derived and not reverseable value.
442 + // TODO: We should skip this update if it was already committed but currently
443 + // we have no way of detecting the difference between a committed and suspended
444 + // update here.
445 + markRenderEventTimeAndConfig(
446 + updateExpirationTime,
447 + update.suspenseConfig,
448 + );
449 +
450 + // Process this update.
451 + newState = getStateFromUpdate(
452 + workInProgress,
453 + queue,
454 + update,
455 + newState,
456 + props,
457 + instance,
458 + );
459 + const callback = update.callback;
460 + if (callback !== null) {
461 + workInProgress.effectTag |= Callback;
462 + let effects = queue.effects;
463 + if (effects === null) {
464 + queue.effects = [update];
465 + } else {
466 + effects.push(update);
467 + }
468 + }
469 }
551 - }
470 + update = update.next;
471 + if (update === null || update === first) {
472 + pendingQueue = queue.shared.pending;
473 + if (pendingQueue === null) {
474 + break;
475 + } else {
476 + // An update was scheduled from inside a reducer. Add the new
477 + // pending updates to the end of the list and keep processing.
478 + update = baseQueue.next = pendingQueue.next;
479 + pendingQueue.next = first;
480 + queue.baseQueue = baseQueue = pendingQueue;
481 + queue.shared.pending = null;
482 + }
483 + }
484 + } while (true);
485 }
553 - update = update.next;
554 - }
486
556 - if (newFirstUpdate === null) {
557 - queue.lastUpdate = null;
558 - }
559 - if (newFirstCapturedUpdate === null) {
560 - queue.lastCapturedUpdate = null;
561 - } else {
562 - workInProgress.effectTag |= Callback;
563 - }
564 - if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
565 - // We processed every update, without skipping. That means the new base
566 - // state is the same as the result state.
567 - newBaseState = resultState;
568 - }
487 + if (newBaseQueueLast === null) {
488 + newBaseState = newState;
489 + } else {
490 + newBaseQueueLast.next = (newBaseQueueFirst: any);
491 + }
492
570 - queue.baseState = newBaseState;
571 - queue.firstUpdate = newFirstUpdate;
572 - queue.firstCapturedUpdate = newFirstCapturedUpdate;
573 -
574 - // Set the remaining expiration time to be whatever is remaining in the queue.
575 - // This should be fine because the only two other things that contribute to
576 - // expiration time are props and context. We're already in the middle of the
577 - // begin phase by the time we start processing the queue, so we've already
578 - // dealt with the props. Context in components that specify
579 - // shouldComponentUpdate is tricky; but we'll have to account for
580 - // that regardless.
581 - markUnprocessedUpdateTime(newExpirationTime);
582 - workInProgress.expirationTime = newExpirationTime;
583 - workInProgress.memoizedState = resultState;
493 + queue.baseState = ((newBaseState: any): State);
494 + queue.baseQueue = newBaseQueueLast;
495 +
496 + // Set the remaining expiration time to be whatever is remaining in the queue.
497 + // This should be fine because the only two other things that contribute to
498 + // expiration time are props and context. We're already in the middle of the
499 + // begin phase by the time we start processing the queue, so we've already
500 + // dealt with the props. Context in components that specify
501 + // shouldComponentUpdate is tricky; but we'll have to account for
502 + // that regardless.
503 + markUnprocessedUpdateTime(newExpirationTime);
504 + workInProgress.expirationTime = newExpirationTime;
505 + workInProgress.memoizedState = newState;
506 + }
507
508 if (__DEV__) {
509 currentlyProcessingQueue = null;
@@ -611,38 +534,17 @@ export function commitUpdateQueue<State>(
534 instance: any,
535 renderExpirationTime: ExpirationTime,
536 ): void {
614 - // If the finished render included captured updates, and there are still
615 - // lower priority updates left over, we need to keep the captured updates
616 - // in the queue so that they are rebased and not dropped once we process the
617 - // queue again at the lower priority.
618 - if (finishedQueue.firstCapturedUpdate !== null) {
619 - // Join the captured update list to the end of the normal list.
620 - if (finishedQueue.lastUpdate !== null) {
621 - finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;
622 - finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;
623 - }
624 - // Clear the list of captured updates.
625 - finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;
626 - }
627 -
537 // Commit the effects
629 - commitUpdateEffects(finishedQueue.firstEffect, instance);
630 - finishedQueue.firstEffect = finishedQueue.lastEffect = null;
631 -
632 - commitUpdateEffects(finishedQueue.firstCapturedEffect, instance);
633 - finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
634 -}
635 -
636 -function commitUpdateEffects<State>(
637 - effect: Update<State> | null,
638 - instance: any,
639 -): void {
640 - while (effect !== null) {
641 - const callback = effect.callback;
642 - if (callback !== null) {
643 - effect.callback = null;
644 - callCallback(callback, instance);
538 + const effects = finishedQueue.effects;
539 + finishedQueue.effects = null;
540 + if (effects !== null) {
541 + for (let i = 0; i < effects.length; i++) {
542 + const effect = effects[i];
543 + const callback = effect.callback;
544 + if (callback !== null) {
545 + effect.callback = null;
546 + callCallback(callback, instance);
547 + }
548 }
646 - effect = effect.nextEffect;
549 }
550 }
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.internal.js
+172
@@ -653,4 +653,176 @@ describe('ReactIncrementalUpdates', () => {
653 expect(Scheduler).toFlushAndYield(['Commit: goodbye']);
654 });
655 });
656 +
657 + it('when rebasing, does not exclude updates that were already committed, regardless of priority', async () => {
658 + const {useState, useLayoutEffect} = React;
659 +
660 + let pushToLog;
661 + function App() {
662 + const [log, setLog] = useState('');
663 + pushToLog = msg => {
664 + setLog(prevLog => prevLog + msg);
665 + };
666 +
667 + useLayoutEffect(
668 + () => {
669 + Scheduler.unstable_yieldValue('Committed: ' + log);
670 + if (log === 'B') {
671 + // Right after B commits, schedule additional updates.
672 + Scheduler.unstable_runWithPriority(
673 + Scheduler.unstable_UserBlockingPriority,
674 + () => {
675 + pushToLog('C');
676 + },
677 + );
678 + setLog(prevLog => prevLog + 'D');
679 + }
680 + },
681 + [log],
682 + );
683 +
684 + return log;
685 + }
686 +
687 + const root = ReactNoop.createRoot();
688 + await ReactNoop.act(async () => {
689 + root.render(<App />);
690 + });
691 + expect(Scheduler).toHaveYielded(['Committed: ']);
692 + expect(root).toMatchRenderedOutput('');
693 +
694 + await ReactNoop.act(async () => {
695 + pushToLog('A');
696 + Scheduler.unstable_runWithPriority(
697 + Scheduler.unstable_UserBlockingPriority,
698 + () => {
699 + pushToLog('B');
700 + },
701 + );
702 + });
703 + expect(Scheduler).toHaveYielded([
704 + // A and B are pending. B is higher priority, so we'll render that first.
705 + 'Committed: B',
706 + // Because A comes first in the queue, we're now in rebase mode. B must
707 + // be rebased on top of A. Also, in a layout effect, we received two new
708 + // updates: C and D. C is user-blocking and D is synchronous.
709 + //
710 + // First render the synchronous update. What we're testing here is that
711 + // B *is not dropped* even though it has lower than sync priority. That's
712 + // because we already committed it. However, this render should not
713 + // include C, because that update wasn't already committed.
714 + 'Committed: BD',
715 + 'Committed: BCD',
716 + 'Committed: ABCD',
717 + ]);
718 + expect(root).toMatchRenderedOutput('ABCD');
719 + });
720 +
721 + it('when rebasing, does not exclude updates that were already committed, regardless of priority (classes)', async () => {
722 + let pushToLog;
723 + class App extends React.Component {
724 + state = {log: ''};
725 + pushToLog = msg => {
726 + this.setState(prevState => ({log: prevState.log + msg}));
727 + };
728 + componentDidUpdate() {
729 + Scheduler.unstable_yieldValue('Committed: ' + this.state.log);
730 + if (this.state.log === 'B') {
731 + // Right after B commits, schedule additional updates.
732 + Scheduler.unstable_runWithPriority(
733 + Scheduler.unstable_UserBlockingPriority,
734 + () => {
735 + this.pushToLog('C');
736 + },
737 + );
738 + this.pushToLog('D');
739 + }
740 + }
741 + render() {
742 + pushToLog = this.pushToLog;
743 + return this.state.log;
744 + }
745 + }
746 +
747 + const root = ReactNoop.createRoot();
748 + await ReactNoop.act(async () => {
749 + root.render(<App />);
750 + });
751 + expect(Scheduler).toHaveYielded([]);
752 + expect(root).toMatchRenderedOutput('');
753 +
754 + await ReactNoop.act(async () => {
755 + pushToLog('A');
756 + Scheduler.unstable_runWithPriority(
757 + Scheduler.unstable_UserBlockingPriority,
758 + () => {
759 + pushToLog('B');
760 + },
761 + );
762 + });
763 + expect(Scheduler).toHaveYielded([
764 + // A and B are pending. B is higher priority, so we'll render that first.
765 + 'Committed: B',
766 + // Because A comes first in the queue, we're now in rebase mode. B must
767 + // be rebased on top of A. Also, in a layout effect, we received two new
768 + // updates: C and D. C is user-blocking and D is synchronous.
769 + //
770 + // First render the synchronous update. What we're testing here is that
771 + // B *is not dropped* even though it has lower than sync priority. That's
772 + // because we already committed it. However, this render should not
773 + // include C, because that update wasn't already committed.
774 + 'Committed: BD',
775 + 'Committed: BCD',
776 + 'Committed: ABCD',
777 + ]);
778 + expect(root).toMatchRenderedOutput('ABCD');
779 + });
780 +
781 + it("base state of update queue is initialized to its fiber's memoized state", async () => {
782 + // This test is very weird because it tests an implementation detail but
783 + // is tested in terms of public APIs. When it was originally written, the
784 + // test failed because the update queue was initialized to the state of
785 + // the alternate fiber.
786 + let app;
787 + class App extends React.Component {
788 + state = {prevProp: 'A', count: 0};
789 + static getDerivedStateFromProps(props, state) {
790 + // Add 100 whenever the label prop changes. The prev label is stored
791 + // in state. If the state is dropped incorrectly, we'll fail to detect
792 + // prop changes.
793 + if (props.prop !== state.prevProp) {
794 + return {
795 + prevProp: props.prop,
796 + count: state.count + 100,
797 + };
798 + }
799 + return null;
800 + }
801 + render() {
802 + app = this;
803 + return this.state.count;
804 + }
805 + }
806 +
807 + const root = ReactNoop.createRoot();
808 + await ReactNoop.act(async () => {
809 + root.render(<App prop="A" />);
810 + });
811 + expect(root).toMatchRenderedOutput('0');
812 +
813 + // Changing the prop causes the count to increase by 100
814 + await ReactNoop.act(async () => {
815 + root.render(<App prop="B" />);
816 + });
817 + expect(root).toMatchRenderedOutput('100');
818 +
819 + // Now increment the count by 1 with a state update. And, in the same
820 + // batch, change the prop back to its original value.
821 + await ReactNoop.act(async () => {
822 + root.render(<App prop="A" />);
823 + app.setState(state => ({count: state.count + 1}));
824 + });
825 + // There were two total prop changes, plus an increment.
826 + expect(root).toMatchRenderedOutput('201');
827 + });
828 });