Skip to content

Commit 3eb1832

Browse files
committed
Entangle overlapping transitions per queue
When multiple transitions update the same queue, only the most recent one should be allowed to finish. We shouldn't show intermediate states. See #17418 for background on why this is important. The way this currently works is that we always assign the same lane to all transitions. It's impossible for one transition to finish without also finishing all the others. The downside of the current approach is that it's too aggressive. Not all transitions are related to each other, so one should not block the other. The new approach is to only entangle transitions if they update one or more of the same state hooks (or class components), because this indicates that they are related. If they are unrelated, then they can finish in any order, as long as they have different lanes. However, this commit does not change anything about how the lanes are assigned. All it does is add the mechanism to entangle per queue. So it doesn't actually change any behavior, yet. But it's a requirement for my next step, which is to assign different lanes to consecutive transitions until we run out and cycle back to the beginning.
1 parent 9c32622 commit 3eb1832

File tree

5 files changed

+100
-11
lines changed

5 files changed

+100
-11
lines changed

packages/react-reconciler/src/ReactFiberClassComponent.new.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040

4141
import {
4242
enqueueUpdate,
43+
entangleTransitions,
4344
processUpdateQueue,
4445
checkHasForceUpdateAfterProcessing,
4546
resetHasForceUpdateBeforeProcessing,
@@ -214,7 +215,10 @@ const classComponentUpdater = {
214215
}
215216

216217
enqueueUpdate(fiber, update, lane);
217-
scheduleUpdateOnFiber(fiber, lane, eventTime);
218+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
219+
if (root !== null) {
220+
entangleTransitions(root, fiber, lane);
221+
}
218222

219223
if (__DEV__) {
220224
if (enableDebugTracing) {
@@ -246,7 +250,10 @@ const classComponentUpdater = {
246250
}
247251

248252
enqueueUpdate(fiber, update, lane);
249-
scheduleUpdateOnFiber(fiber, lane, eventTime);
253+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
254+
if (root !== null) {
255+
entangleTransitions(root, fiber, lane);
256+
}
250257

251258
if (__DEV__) {
252259
if (enableDebugTracing) {
@@ -277,7 +284,10 @@ const classComponentUpdater = {
277284
}
278285

279286
enqueueUpdate(fiber, update, lane);
280-
scheduleUpdateOnFiber(fiber, lane, eventTime);
287+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
288+
if (root !== null) {
289+
entangleTransitions(root, fiber, lane);
290+
}
281291

282292
if (__DEV__) {
283293
if (enableDebugTracing) {

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import {
4545
isSubsetOfLanes,
4646
mergeLanes,
4747
removeLanes,
48+
intersectLanes,
49+
isTransitionLane,
4850
markRootEntangled,
4951
markRootMutableRead,
5052
getCurrentUpdateLanePriority,
@@ -104,7 +106,11 @@ import {getIsRendering} from './ReactCurrentFiber';
104106
import {logStateUpdateScheduled} from './DebugTracing';
105107
import {markStateUpdateScheduled} from './SchedulingProfiler';
106108
import {CacheContext} from './ReactFiberCacheComponent.new';
107-
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
109+
import {
110+
createUpdate,
111+
enqueueUpdate,
112+
entangleTransitions,
113+
} from './ReactUpdateQueue.new';
108114
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
109115

110116
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -121,6 +127,7 @@ type Update<S, A> = {|
121127
export type UpdateQueue<S, A> = {|
122128
pending: Update<S, A> | null,
123129
interleaved: Update<S, A> | null,
130+
lanes: Lanes,
124131
dispatch: (A => mixed) | null,
125132
lastRenderedReducer: ((S, A) => S) | null,
126133
lastRenderedState: S | null,
@@ -654,6 +661,7 @@ function mountReducer<S, I, A>(
654661
const queue = (hook.queue = {
655662
pending: null,
656663
interleaved: null,
664+
lanes: NoLanes,
657665
dispatch: null,
658666
lastRenderedReducer: reducer,
659667
lastRenderedState: (initialState: any),
@@ -811,6 +819,10 @@ function updateReducer<S, I, A>(
811819
markSkippedUpdateLanes(interleavedLane);
812820
interleaved = ((interleaved: any).next: Update<S, A>);
813821
} while (interleaved !== lastInterleaved);
822+
} else if (baseQueue === null) {
823+
// `queue.lanes` is used for entangling transitions. We can set it back to
824+
// zero once the queue is empty.
825+
queue.lanes = NoLanes;
814826
}
815827

816828
const dispatch: Dispatch<A> = (queue.dispatch: any);
@@ -1102,6 +1114,7 @@ function useMutableSource<Source, Snapshot>(
11021114
const newQueue = {
11031115
pending: null,
11041116
interleaved: null,
1117+
lanes: NoLanes,
11051118
dispatch: null,
11061119
lastRenderedReducer: basicStateReducer,
11071120
lastRenderedState: snapshot,
@@ -1158,6 +1171,7 @@ function mountState<S>(
11581171
const queue = (hook.queue = {
11591172
pending: null,
11601173
interleaved: null,
1174+
lanes: NoLanes,
11611175
dispatch: null,
11621176
lastRenderedReducer: basicStateReducer,
11631177
lastRenderedState: (initialState: any),
@@ -1821,6 +1835,9 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
18211835
const lane = requestUpdateLane(provider);
18221836
const eventTime = requestEventTime();
18231837
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
1838+
if (root !== null) {
1839+
entangleTransitions(root, fiber, lane);
1840+
}
18241841

18251842
const seededCache = new Map();
18261843
if (seedKey !== null && seedKey !== undefined && root !== null) {
@@ -1960,7 +1977,22 @@ function dispatchAction<S, A>(
19601977
warnIfNotCurrentlyActingUpdatesInDev(fiber);
19611978
}
19621979
}
1963-
scheduleUpdateOnFiber(fiber, lane, eventTime);
1980+
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
1981+
1982+
if (isTransitionLane(lane) && root !== null) {
1983+
let queueLanes = queue.lanes;
1984+
1985+
// If any entangled lanes are no longer pending on the root, then they
1986+
// must have finished. We can remove them from the
1987+
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
1988+
1989+
// Entangle the new transition lane with the other transition lanes.
1990+
const newQueueLanes = mergeLanes(queueLanes, lane);
1991+
if (newQueueLanes !== queueLanes) {
1992+
queue.lanes = newQueueLanes;
1993+
markRootEntangled(root, newQueueLanes);
1994+
}
1995+
}
19641996
}
19651997

19661998
if (__DEV__) {

packages/react-reconciler/src/ReactFiberLane.new.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
310310
}
311311

312312
if (enableTransitionEntanglement) {
313-
// We don't need to include higher priority lanes, because in this
314-
// experiment we always unsuspend all transitions whenever we receive
315-
// an update.
313+
// We don't need to do anything extra here, because we apply per-lane
314+
// transition entanglement in the entanglement loop below.
316315
} else {
317316
// If there are higher priority lanes, we'll include them even if they
318317
// are suspended.
@@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) {
492491
return (lanes & TransitionLanes) === lanes;
493492
}
494493

494+
export function isTransitionLane(lane: Lane) {
495+
return (lane & TransitionLanes) !== 0;
496+
}
497+
495498
// To ensure consistency across multiple updates in the same event, this should
496499
// be a pure function, so that it always returns the same lane for given inputs.
497500
export function findUpdateLane(
@@ -634,6 +637,10 @@ export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
634637
return set & ~subset;
635638
}
636639

640+
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
641+
return a & b;
642+
}
643+
637644
// Seems redundant, but it changes the type from a single lane (used for
638645
// updates) to a group of lanes (used for flushing work).
639646
export function laneToLanes(lane: Lane): Lanes {

packages/react-reconciler/src/ReactFiberReconciler.new.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ import {
6565
IsThisRendererActing,
6666
act,
6767
} from './ReactFiberWorkLoop.new';
68-
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
68+
import {
69+
createUpdate,
70+
enqueueUpdate,
71+
entangleTransitions,
72+
} from './ReactUpdateQueue.new';
6973
import {
7074
isRendering as ReactCurrentFiberIsRendering,
7175
current as ReactCurrentFiberCurrent,
@@ -315,7 +319,10 @@ export function updateContainer(
315319
}
316320

317321
enqueueUpdate(current, update, lane);
318-
scheduleUpdateOnFiber(current, lane, eventTime);
322+
const root = scheduleUpdateOnFiber(current, lane, eventTime);
323+
if (root !== null) {
324+
entangleTransitions(root, current, lane);
325+
}
319326

320327
return lane;
321328
}

packages/react-reconciler/src/ReactUpdateQueue.new.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,17 @@
8484
// regardless of priority. Intermediate state may vary according to system
8585
// resources, but the final state is always the same.
8686

87-
import type {Fiber} from './ReactInternalTypes';
87+
import type {Fiber, FiberRoot} from './ReactInternalTypes';
8888
import type {Lanes, Lane} from './ReactFiberLane.new';
8989

9090
import {
9191
NoLane,
9292
NoLanes,
9393
isSubsetOfLanes,
9494
mergeLanes,
95+
isTransitionLane,
96+
intersectLanes,
97+
markRootEntangled,
9598
} from './ReactFiberLane.new';
9699
import {
97100
enterDisallowedContextReadInDEV,
@@ -128,6 +131,7 @@ export type Update<State> = {|
128131
export type SharedQueue<State> = {|
129132
pending: Update<State> | null,
130133
interleaved: Update<State> | null,
134+
lanes: Lanes,
131135
|};
132136

133137
export type UpdateQueue<State> = {|
@@ -167,6 +171,7 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
167171
shared: {
168172
pending: null,
169173
interleaved: null,
174+
lanes: NoLanes,
170175
},
171176
effects: null,
172177
};
@@ -260,6 +265,30 @@ export function enqueueUpdate<State>(
260265
}
261266
}
262267

268+
export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
269+
const updateQueue = fiber.updateQueue;
270+
if (updateQueue === null) {
271+
// Only occurs if the fiber has been unmounted.
272+
return;
273+
}
274+
275+
const sharedQueue: SharedQueue<mixed> = (updateQueue: any).shared;
276+
if (isTransitionLane(lane) && root !== null) {
277+
let queueLanes = sharedQueue.lanes;
278+
279+
// If any entangled lanes are no longer pending on the root, then they
280+
// must have finished. We can remove them from the
281+
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
282+
283+
// Entangle the new transition lane with the other transition lanes.
284+
const newQueueLanes = mergeLanes(queueLanes, lane);
285+
if (newQueueLanes !== queueLanes) {
286+
sharedQueue.lanes = newQueueLanes;
287+
markRootEntangled(root, newQueueLanes);
288+
}
289+
}
290+
}
291+
263292
export function enqueueCapturedUpdate<State>(
264293
workInProgress: Fiber,
265294
capturedUpdate: Update<State>,
@@ -595,6 +624,10 @@ export function processUpdateQueue<State>(
595624
newLanes = mergeLanes(newLanes, interleaved.lane);
596625
interleaved = ((interleaved: any).next: Update<State>);
597626
} while (interleaved !== lastInterleaved);
627+
} else if (firstBaseUpdate === null) {
628+
// `queue.lanes` is used for entangling transitions. We can set it back to
629+
// zero once the queue is empty.
630+
queue.shared.lanes = NoLanes;
598631
}
599632

600633
// Set the remaining expiration time to be whatever is remaining in the queue.

0 commit comments

Comments
 (0)