Skip to content

Commit 66066ea

Browse files
acdlitesebmarkbage
andcommitted
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: - facebook#17483 by @sebmarkbage refactors the hook update queue - facebook#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 <[email protected]> Co-authored-by: Andrew Clark <[email protected]>
1 parent e039e69 commit 66066ea

File tree

5 files changed

+479
-344
lines changed

5 files changed

+479
-344
lines changed

packages/react-noop-renderer/src/createReactNoop.js

+24-11
Original file line numberDiff line numberDiff line change
@@ -1142,20 +1142,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
11421142

11431143
function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
11441144
log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
1145-
const firstUpdate = updateQueue.firstUpdate;
1146-
if (!firstUpdate) {
1145+
const last = updateQueue.baseQueue;
1146+
if (last === null) {
11471147
return;
11481148
}
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+
}
11491159

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+
}
11591172
}
11601173
}
11611174

packages/react-reconciler/src/ReactFiberHooks.js

+70-49
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
2020

2121
import ReactSharedInternals from 'shared/ReactSharedInternals';
2222

23-
import {NoWork} from './ReactFiberExpirationTime';
23+
import {NoWork, Sync} from './ReactFiberExpirationTime';
2424
import {readContext} from './ReactFiberNewContext';
2525
import {createResponderListener} from './ReactFiberEvents';
2626
import {
@@ -108,13 +108,13 @@ type Update<S, A> = {
108108
action: A,
109109
eagerReducer: ((S, A) => S) | null,
110110
eagerState: S | null,
111-
next: Update<S, A> | null,
111+
next: Update<S, A>,
112112

113113
priority?: ReactPriorityLevel,
114114
};
115115

116116
type UpdateQueue<S, A> = {
117-
last: Update<S, A> | null,
117+
pending: Update<S, A> | null,
118118
dispatch: (A => mixed) | null,
119119
lastRenderedReducer: ((S, A) => S) | null,
120120
lastRenderedState: S | null,
@@ -144,7 +144,7 @@ export type Hook = {
144144
memoizedState: any,
145145

146146
baseState: any,
147-
baseUpdate: Update<any, any> | null,
147+
baseQueue: Update<any, any> | null,
148148
queue: UpdateQueue<any, any> | null,
149149

150150
next: Hook | null,
@@ -544,8 +544,8 @@ function mountWorkInProgressHook(): Hook {
544544
memoizedState: null,
545545

546546
baseState: null,
547+
baseQueue: null,
547548
queue: null,
548-
baseUpdate: null,
549549

550550
next: null,
551551
};
@@ -604,8 +604,8 @@ function updateWorkInProgressHook(): Hook {
604604
memoizedState: currentHook.memoizedState,
605605

606606
baseState: currentHook.baseState,
607+
baseQueue: currentHook.baseQueue,
607608
queue: currentHook.queue,
608-
baseUpdate: currentHook.baseUpdate,
609609

610610
next: null,
611611
};
@@ -645,7 +645,7 @@ function mountReducer<S, I, A>(
645645
}
646646
hook.memoizedState = hook.baseState = initialState;
647647
const queue = (hook.queue = {
648-
last: null,
648+
pending: null,
649649
dispatch: null,
650650
lastRenderedReducer: reducer,
651651
lastRenderedState: (initialState: any),
@@ -703,7 +703,7 @@ function updateReducer<S, I, A>(
703703
// the base state unless the queue is empty.
704704
// TODO: Not sure if this is the desired semantics, but it's what we
705705
// do for gDSFP. I can't remember why.
706-
if (hook.baseUpdate === queue.last) {
706+
if (hook.baseQueue === null) {
707707
hook.baseState = newState;
708708
}
709709

@@ -715,42 +715,55 @@ function updateReducer<S, I, A>(
715715
return [hook.memoizedState, dispatch];
716716
}
717717

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;
732734
}
733-
first = baseUpdate.next;
734-
} else {
735-
first = last !== null ? last.next : null;
735+
current.baseQueue = baseQueue = pendingQueue;
736+
queue.pending = null;
736737
}
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+
739744
let newBaseState = null;
740-
let newBaseUpdate = null;
741-
let prevUpdate = baseUpdate;
745+
let newBaseQueueFirst = null;
746+
let newBaseQueueLast = null;
742747
let update = first;
743-
let didSkip = false;
744748
do {
745749
const updateExpirationTime = update.expirationTime;
746750
if (updateExpirationTime < renderExpirationTime) {
747751
// Priority is insufficient. Skip this update. If this is the first
748752
// skipped update, the previous update/state is the new base
749753
// 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;
753764
newBaseState = newState;
765+
} else {
766+
newBaseQueueLast = newBaseQueueLast.next = clone;
754767
}
755768
// Update the remaining priority in the queue.
756769
if (updateExpirationTime > currentlyRenderingFiber.expirationTime) {
@@ -760,6 +773,18 @@ function updateReducer<S, I, A>(
760773
} else {
761774
// This update does have sufficient priority.
762775

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+
763788
// Mark the event time of this update as relevant to this render pass.
764789
// TODO: This should ideally use the true event time of this update rather than
765790
// its priority which is a derived and not reverseable value.
@@ -781,13 +806,13 @@ function updateReducer<S, I, A>(
781806
newState = reducer(newState, action);
782807
}
783808
}
784-
prevUpdate = update;
785809
update = update.next;
786810
} while (update !== null && update !== first);
787811

788-
if (!didSkip) {
789-
newBaseUpdate = prevUpdate;
812+
if (newBaseQueueLast === null) {
790813
newBaseState = newState;
814+
} else {
815+
newBaseQueueLast.next = (newBaseQueueFirst: any);
791816
}
792817

793818
// Mark that the fiber performed work, but only if the new state is
@@ -797,8 +822,8 @@ function updateReducer<S, I, A>(
797822
}
798823

799824
hook.memoizedState = newState;
800-
hook.baseUpdate = newBaseUpdate;
801825
hook.baseState = newBaseState;
826+
hook.baseQueue = newBaseQueueLast;
802827

803828
queue.lastRenderedState = newState;
804829
}
@@ -816,7 +841,7 @@ function mountState<S>(
816841
}
817842
hook.memoizedState = hook.baseState = initialState;
818843
const queue = (hook.queue = {
819-
last: null,
844+
pending: null,
820845
dispatch: null,
821846
lastRenderedReducer: basicStateReducer,
822847
lastRenderedState: (initialState: any),
@@ -1233,7 +1258,7 @@ function dispatchAction<S, A>(
12331258
action,
12341259
eagerReducer: null,
12351260
eagerState: null,
1236-
next: null,
1261+
next: (null: any),
12371262
};
12381263
if (__DEV__) {
12391264
update.priority = getCurrentPriorityLevel();
@@ -1267,27 +1292,23 @@ function dispatchAction<S, A>(
12671292
action,
12681293
eagerReducer: null,
12691294
eagerState: null,
1270-
next: null,
1295+
next: (null: any),
12711296
};
12721297

12731298
if (__DEV__) {
12741299
update.priority = getCurrentPriorityLevel();
12751300
}
12761301

12771302
// 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) {
12801305
// This is the first update. Create a circular list.
12811306
update.next = update;
12821307
} 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;
12891310
}
1290-
queue.last = update;
1311+
queue.pending = update;
12911312

12921313
if (
12931314
fiber.expirationTime === NoWork &&

packages/react-reconciler/src/ReactFiberWorkLoop.js

+9-10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
1414
import type {Interaction} from 'scheduler/src/Tracing';
1515
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
1616
import type {SuspenseState} from './ReactFiberSuspenseComponent';
17+
import type {Hook} from './ReactFiberHooks';
1718

1819
import {
1920
warnAboutDeprecatedLifecycles,
@@ -2859,7 +2860,7 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
28592860
// has triggered any high priority updates
28602861
const updateQueue = current.updateQueue;
28612862
if (updateQueue !== null) {
2862-
let update = updateQueue.firstUpdate;
2863+
let update = updateQueue.baseQueue;
28632864
while (update !== null) {
28642865
const priorityLevel = update.priority;
28652866
if (
@@ -2883,12 +2884,11 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
28832884
break;
28842885
case FunctionComponent:
28852886
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;
28922892
// Loop through the functional component's memoized state to see whether
28932893
// the component has triggered any high pri updates
28942894
while (update !== null) {
@@ -2908,15 +2908,14 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
29082908
}
29092909
break;
29102910
}
2911-
if (
2912-
update.next === workInProgressNode.memoizedState.baseUpdate
2913-
) {
2911+
if (update.next === firstHook.baseQueue) {
29142912
break;
29152913
}
29162914
update = update.next;
29172915
}
29182916
}
29192917
break;
2918+
}
29202919
default:
29212920
break;
29222921
}

0 commit comments

Comments
 (0)