Land forked reconciler changes (#24817)
This applies forked changes from the "new" reconciler to the "old" one. Includes: - d410f0a [FORKED] Bugfix: Offscreen instance is null during setState - 58bb117 [FORKED] Check for infinite update loops even if unmounted - 31882b5 [FORKED] Bugfix: Revealing a hidden update - 17691ac [FORKED] Don't update childLanes until after current render
Andrew Clark committed
Jun 29, 2022 at 23:31 UTC
6b6cf8311c05a10667c852e833d6f7f51fa476c7
12 files changed
+307
-191
packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js
+20
-15
@@ -90,8 +90,10 @@ import type {Lanes, Lane} from './ReactFiberLane.old';
90
import {
91
NoLane,
92
NoLanes,
93
+ OffscreenLane,
94
isSubsetOfLanes,
95
mergeLanes,
96
+ removeLanes,
97
isTransitionLane,
98
intersectLanes,
99
markRootEntangled,
@@ -108,6 +110,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
110
import {
111
markSkippedUpdateLanes,
112
isUnsafeClassRenderPhaseUpdate,
113
+ getWorkInProgressRootRenderLanes,
114
} from './ReactFiberWorkLoop.old';
115
import {
116
enqueueConcurrentClassUpdate,
@@ -132,7 +135,6 @@ export type Update<State> = {|
135
136
export type SharedQueue<State> = {|
137
pending: Update<State> | null,
135
- interleaved: Update<State> | null,
138
lanes: Lanes,
139
|};
140
@@ -172,7 +174,6 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
174
lastBaseUpdate: null,
175
shared: {
176
pending: null,
175
- interleaved: null,
177
lanes: NoLanes,
178
},
179
effects: null,
@@ -525,9 +526,23 @@ export function processUpdateQueue<State>(
526
527
let update = firstBaseUpdate;
528
do {
528
- const updateLane = update.lane;
529
+ // TODO: Don't need this field anymore
530
const updateEventTime = update.eventTime;
530
- if (!isSubsetOfLanes(renderLanes, updateLane)) {
531
+
532
+ // An extra OffscreenLane bit is added to updates that were made to
533
+ // a hidden tree, so that we can distinguish them from updates that were
534
+ // already there when the tree was hidden.
535
+ const updateLane = removeLanes(update.lane, OffscreenLane);
536
+ const isHiddenUpdate = updateLane !== update.lane;
537
+
538
+ // Check if this update was made while the tree was hidden. If so, then
539
+ // it's not a "base" update and we should disregard the extra base lanes
540
+ // that were added to renderLanes when we entered the Offscreen tree.
541
+ const shouldSkipUpdate = isHiddenUpdate
542
+ ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
543
+ : !isSubsetOfLanes(renderLanes, updateLane);
544
+
545
+ if (shouldSkipUpdate) {
546
// Priority is insufficient. Skip this update. If this is the first
547
// skipped update, the previous update/state is the new base
548
// update/state.
@@ -622,17 +637,7 @@ export function processUpdateQueue<State>(
637
queue.firstBaseUpdate = newFirstBaseUpdate;
638
queue.lastBaseUpdate = newLastBaseUpdate;
639
625
- // Interleaved updates are stored on a separate queue. We aren't going to
626
- // process them during this render, but we do need to track which lanes
627
- // are remaining.
628
- const lastInterleaved = queue.shared.interleaved;
629
- if (lastInterleaved !== null) {
630
- let interleaved = lastInterleaved;
631
- do {
632
- newLanes = mergeLanes(newLanes, interleaved.lane);
633
- interleaved = ((interleaved: any).next: Update<State>);
634
- } while (interleaved !== lastInterleaved);
635
- } else if (firstBaseUpdate === null) {
640
+ if (firstBaseUpdate === null) {
641
// `queue.lanes` is used for entangling transitions. We can set it back to
642
// zero once the queue is empty.
643
queue.shared.lanes = NoLanes;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+9
-11
@@ -1522,17 +1522,15 @@ function completeWork(
1522
// at offscreen priority.
1523
if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
1524
bubbleProperties(workInProgress);
1525
- if (supportsMutation) {
1526
- // Check if there was an insertion or update in the hidden subtree.
1527
- // If so, we need to hide those nodes in the commit phase, so
1528
- // schedule a visibility effect.
1529
- if (
1530
- (!enableLegacyHidden ||
1531
- workInProgress.tag !== LegacyHiddenComponent) &&
1532
- workInProgress.subtreeFlags & (Placement | Update)
1533
- ) {
1534
- workInProgress.flags |= Visibility;
1535
- }
1525
+ // Check if there was an insertion or update in the hidden subtree.
1526
+ // If so, we need to hide those nodes in the commit phase, so
1527
+ // schedule a visibility effect.
1528
+ if (
1529
+ (!enableLegacyHidden ||
1530
+ workInProgress.tag !== LegacyHiddenComponent) &&
1531
+ workInProgress.subtreeFlags & (Placement | Update)
1532
+ ) {
1533
+ workInProgress.flags |= Visibility;
1534
}
1535
}
1536
}
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+9
-11
@@ -1522,17 +1522,15 @@ function completeWork(
1522
// at offscreen priority.
1523
if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
1524
bubbleProperties(workInProgress);
1525
- if (supportsMutation) {
1526
- // Check if there was an insertion or update in the hidden subtree.
1527
- // If so, we need to hide those nodes in the commit phase, so
1528
- // schedule a visibility effect.
1529
- if (
1530
- (!enableLegacyHidden ||
1531
- workInProgress.tag !== LegacyHiddenComponent) &&
1532
- workInProgress.subtreeFlags & (Placement | Update)
1533
- ) {
1534
- workInProgress.flags |= Visibility;
1535
- }
1525
+ // Check if there was an insertion or update in the hidden subtree.
1526
+ // If so, we need to hide those nodes in the commit phase, so
1527
+ // schedule a visibility effect.
1528
+ if (
1529
+ (!enableLegacyHidden ||
1530
+ workInProgress.tag !== LegacyHiddenComponent) &&
1531
+ workInProgress.subtreeFlags & (Placement | Update)
1532
+ ) {
1533
+ workInProgress.flags |= Visibility;
1534
}
1535
}
1536
}
packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js
+7
-1
@@ -166,8 +166,14 @@ export function unsafe_markUpdateLaneFromFiberToRoot(
166
sourceFiber: Fiber,
167
lane: Lane,
168
): FiberRoot | null {
169
+ // NOTE: For Hyrum's Law reasons, if an infinite update loop is detected, it
170
+ // should throw before `markUpdateLaneFromFiberToRoot` is called. But this is
171
+ // undefined behavior and we can change it if we need to; it just so happens
172
+ // that, at the time of this writing, there's an internal product test that
173
+ // happens to rely on this.
174
+ const root = getRootForUpdatedFiber(sourceFiber);
175
markUpdateLaneFromFiberToRoot(sourceFiber, null, lane);
170
- return getRootForUpdatedFiber(sourceFiber);
176
+ return root;
177
}
178
179
function markUpdateLaneFromFiberToRoot(
packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js
+191
-114
@@ -16,100 +16,128 @@ import type {
16
SharedQueue as ClassQueue,
17
Update as ClassUpdate,
18
} from './ReactFiberClassUpdateQueue.old';
19
-import type {Lane} from './ReactFiberLane.old';
19
+import type {Lane, Lanes} from './ReactFiberLane.old';
20
+import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
21
22
import {
23
warnAboutUpdateOnNotYetMountedFiberInDEV,
24
throwIfInfiniteUpdateLoopDetected,
25
} from './ReactFiberWorkLoop.old';
25
-import {mergeLanes} from './ReactFiberLane.old';
26
+import {
27
+ NoLane,
28
+ NoLanes,
29
+ mergeLanes,
30
+ markHiddenUpdate,
31
+} from './ReactFiberLane.old';
32
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
27
-import {HostRoot} from './ReactWorkTags';
28
-
29
-// An array of all update queues that received updates during the current
30
-// render. When this render exits, either because it finishes or because it is
31
-// interrupted, the interleaved updates will be transferred onto the main part
32
-// of the queue.
33
-let concurrentQueues: Array<
34
- HookQueue<any, any> | ClassQueue<any>,
35
-> | null = null;
36
-
37
-export function pushConcurrentUpdateQueue(
38
- queue: HookQueue<any, any> | ClassQueue<any>,
39
-) {
40
- if (concurrentQueues === null) {
41
- concurrentQueues = [queue];
42
- } else {
43
- concurrentQueues.push(queue);
44
- }
45
-}
33
+import {HostRoot, OffscreenComponent} from './ReactWorkTags';
34
+
35
+export type ConcurrentUpdate = {
36
+ next: ConcurrentUpdate,
37
+ lane: Lane,
38
+};
39
+
40
+type ConcurrentQueue = {
41
+ pending: ConcurrentUpdate | null,
42
+};
43
+
44
+// If a render is in progress, and we receive an update from a concurrent event,
45
+// we wait until the current render is over (either finished or interrupted)
46
+// before adding it to the fiber/hook queue. Push to this array so we can
47
+// access the queue, fiber, update, et al later.
48
+const concurrentQueues: Array<any> = [];
49
+let concurrentQueuesIndex = 0;
50
+
51
+let concurrentlyUpdatedLanes: Lanes = NoLanes;
52
+
53
+export function finishQueueingConcurrentUpdates(): void {
54
+ const endIndex = concurrentQueuesIndex;
55
+ concurrentQueuesIndex = 0;
56
+
57
+ concurrentlyUpdatedLanes = NoLanes;
58
47
-export function finishQueueingConcurrentUpdates() {
48
- // Transfer the interleaved updates onto the main queue. Each queue has a
49
- // `pending` field and an `interleaved` field. When they are not null, they
50
- // point to the last node in a circular linked list. We need to append the
51
- // interleaved list to the end of the pending list by joining them into a
52
- // single, circular list.
53
- if (concurrentQueues !== null) {
54
- for (let i = 0; i < concurrentQueues.length; i++) {
55
- const queue = concurrentQueues[i];
56
- const lastInterleavedUpdate = queue.interleaved;
57
- if (lastInterleavedUpdate !== null) {
58
- queue.interleaved = null;
59
- const firstInterleavedUpdate = lastInterleavedUpdate.next;
60
- const lastPendingUpdate = queue.pending;
61
- if (lastPendingUpdate !== null) {
62
- const firstPendingUpdate = lastPendingUpdate.next;
63
- lastPendingUpdate.next = (firstInterleavedUpdate: any);
64
- lastInterleavedUpdate.next = (firstPendingUpdate: any);
65
- }
66
- queue.pending = (lastInterleavedUpdate: any);
59
+ let i = 0;
60
+ while (i < endIndex) {
61
+ const fiber: Fiber = concurrentQueues[i];
62
+ concurrentQueues[i++] = null;
63
+ const queue: ConcurrentQueue = concurrentQueues[i];
64
+ concurrentQueues[i++] = null;
65
+ const update: ConcurrentUpdate = concurrentQueues[i];
66
+ concurrentQueues[i++] = null;
67
+ const lane: Lane = concurrentQueues[i];
68
+ concurrentQueues[i++] = null;
69
+
70
+ if (queue !== null && update !== null) {
71
+ const pending = queue.pending;
72
+ if (pending === null) {
73
+ // This is the first update. Create a circular list.
74
+ update.next = update;
75
+ } else {
76
+ update.next = pending.next;
77
+ pending.next = update;
78
}
79
+ queue.pending = update;
80
+ }
81
+
82
+ if (lane !== NoLane) {
83
+ markUpdateLaneFromFiberToRoot(fiber, update, lane);
84
}
69
- concurrentQueues = null;
85
}
86
}
87
73
-export function enqueueConcurrentHookUpdate<S, A>(
88
+export function getConcurrentlyUpdatedLanes(): Lanes {
89
+ return concurrentlyUpdatedLanes;
90
+}
91
+
92
+function enqueueUpdate(
93
fiber: Fiber,
75
- queue: HookQueue<S, A>,
76
- update: HookUpdate<S, A>,
94
+ queue: ConcurrentQueue | null,
95
+ update: ConcurrentUpdate | null,
96
lane: Lane,
97
) {
79
- const interleaved = queue.interleaved;
80
- if (interleaved === null) {
81
- // This is the first update. Create a circular list.
82
- update.next = update;
83
- // At the end of the current render, this queue's interleaved updates will
84
- // be transferred to the pending queue.
85
- pushConcurrentUpdateQueue(queue);
86
- } else {
87
- update.next = interleaved.next;
88
- interleaved.next = update;
98
+ // Don't update the `childLanes` on the return path yet. If we already in
99
+ // the middle of rendering, wait until after it has completed.
100
+ concurrentQueues[concurrentQueuesIndex++] = fiber;
101
+ concurrentQueues[concurrentQueuesIndex++] = queue;
102
+ concurrentQueues[concurrentQueuesIndex++] = update;
103
+ concurrentQueues[concurrentQueuesIndex++] = lane;
104
+
105
+ concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
106
+
107
+ // The fiber's `lane` field is used in some places to check if any work is
108
+ // scheduled, to perform an eager bailout, so we need to update it immediately.
109
+ // TODO: We should probably move this to the "shared" queue instead.
110
+ fiber.lanes = mergeLanes(fiber.lanes, lane);
111
+ const alternate = fiber.alternate;
112
+ if (alternate !== null) {
113
+ alternate.lanes = mergeLanes(alternate.lanes, lane);
114
}
90
- queue.interleaved = update;
115
+}
116
92
- return markUpdateLaneFromFiberToRoot(fiber, lane);
117
+export function enqueueConcurrentHookUpdate<S, A>(
118
+ fiber: Fiber,
119
+ queue: HookQueue<S, A>,
120
+ update: HookUpdate<S, A>,
121
+ lane: Lane,
122
+): FiberRoot | null {
123
+ const concurrentQueue: ConcurrentQueue = (queue: any);
124
+ const concurrentUpdate: ConcurrentUpdate = (update: any);
125
+ enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
126
+ return getRootForUpdatedFiber(fiber);
127
}
128
129
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
130
fiber: Fiber,
131
queue: HookQueue<S, A>,
132
update: HookUpdate<S, A>,
99
- lane: Lane,
133
): void {
101
- const interleaved = queue.interleaved;
102
- if (interleaved === null) {
103
- // This is the first update. Create a circular list.
104
- update.next = update;
105
- // At the end of the current render, this queue's interleaved updates will
106
- // be transferred to the pending queue.
107
- pushConcurrentUpdateQueue(queue);
108
- } else {
109
- update.next = interleaved.next;
110
- interleaved.next = update;
111
- }
112
- queue.interleaved = update;
134
+ // This function is used to queue an update that doesn't need a rerender. The
135
+ // only reason we queue it is in case there's a subsequent higher priority
136
+ // update that causes it to be rebased.
137
+ const lane = NoLane;
138
+ const concurrentQueue: ConcurrentQueue = (queue: any);
139
+ const concurrentUpdate: ConcurrentUpdate = (update: any);
140
+ enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
141
}
142
143
export function enqueueConcurrentClassUpdate<State>(
@@ -117,78 +145,127 @@ export function enqueueConcurrentClassUpdate<State>(
145
queue: ClassQueue<State>,
146
update: ClassUpdate<State>,
147
lane: Lane,
120
-) {
121
- const interleaved = queue.interleaved;
122
- if (interleaved === null) {
123
- // This is the first update. Create a circular list.
124
- update.next = update;
125
- // At the end of the current render, this queue's interleaved updates will
126
- // be transferred to the pending queue.
127
- pushConcurrentUpdateQueue(queue);
128
- } else {
129
- update.next = interleaved.next;
130
- interleaved.next = update;
131
- }
132
- queue.interleaved = update;
133
-
134
- return markUpdateLaneFromFiberToRoot(fiber, lane);
148
+): FiberRoot | null {
149
+ const concurrentQueue: ConcurrentQueue = (queue: any);
150
+ const concurrentUpdate: ConcurrentUpdate = (update: any);
151
+ enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
152
+ return getRootForUpdatedFiber(fiber);
153
}
154
137
-export function enqueueConcurrentRenderForLane(fiber: Fiber, lane: Lane) {
138
- return markUpdateLaneFromFiberToRoot(fiber, lane);
155
+export function enqueueConcurrentRenderForLane(
156
+ fiber: Fiber,
157
+ lane: Lane,
158
+): FiberRoot | null {
159
+ enqueueUpdate(fiber, null, null, lane);
160
+ return getRootForUpdatedFiber(fiber);
161
}
162
163
// Calling this function outside this module should only be done for backwards
164
// compatibility and should always be accompanied by a warning.
143
-export const unsafe_markUpdateLaneFromFiberToRoot = markUpdateLaneFromFiberToRoot;
144
-
145
-function markUpdateLaneFromFiberToRoot(
165
+export function unsafe_markUpdateLaneFromFiberToRoot(
166
sourceFiber: Fiber,
167
lane: Lane,
168
): FiberRoot | null {
149
- // TODO: We will detect and infinite update loop and throw even if this fiber
150
- // has already unmounted. This isn't really necessary but it happens to be the
151
- // current behavior we've used for several release cycles. Consider not
152
- // performing this check if the updated fiber already unmounted, since it's
153
- // not possible for that to cause an infinite update loop.
154
- throwIfInfiniteUpdateLoopDetected();
169
+ // NOTE: For Hyrum's Law reasons, if an infinite update loop is detected, it
170
+ // should throw before `markUpdateLaneFromFiberToRoot` is called. But this is
171
+ // undefined behavior and we can change it if we need to; it just so happens
172
+ // that, at the time of this writing, there's an internal product test that
173
+ // happens to rely on this.
174
+ const root = getRootForUpdatedFiber(sourceFiber);
175
+ markUpdateLaneFromFiberToRoot(sourceFiber, null, lane);
176
+ return root;
177
+}
178
179
+function markUpdateLaneFromFiberToRoot(
180
+ sourceFiber: Fiber,
181
+ update: ConcurrentUpdate | null,
182
+ lane: Lane,
183
+): void {
184
// Update the source fiber's lanes
185
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
186
let alternate = sourceFiber.alternate;
187
if (alternate !== null) {
188
alternate.lanes = mergeLanes(alternate.lanes, lane);
189
}
162
- if (__DEV__) {
163
- if (
164
- alternate === null &&
165
- (sourceFiber.flags & (Placement | Hydrating)) !== NoFlags
166
- ) {
167
- warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
168
- }
169
- }
190
// Walk the parent path to the root and update the child lanes.
171
- let node = sourceFiber;
191
+ let isHidden = false;
192
let parent = sourceFiber.return;
193
+ let node = sourceFiber;
194
while (parent !== null) {
195
parent.childLanes = mergeLanes(parent.childLanes, lane);
196
alternate = parent.alternate;
197
if (alternate !== null) {
198
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
178
- } else {
179
- if (__DEV__) {
180
- if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
181
- warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
182
- }
199
+ }
200
+
201
+ if (parent.tag === OffscreenComponent) {
202
+ // Check if this offscreen boundary is currently hidden.
203
+ //
204
+ // The instance may be null if the Offscreen parent was unmounted. Usually
205
+ // the parent wouldn't be reachable in that case because we disconnect
206
+ // fibers from the tree when they are deleted. However, there's a weird
207
+ // edge case where setState is called on a fiber that was interrupted
208
+ // before it ever mounted. Because it never mounts, it also never gets
209
+ // deleted. Because it never gets deleted, its return pointer never gets
210
+ // disconnected. Which means it may be attached to a deleted Offscreen
211
+ // parent node. (This discovery suggests it may be better for memory usage
212
+ // if we don't attach the `return` pointer until the commit phase, though
213
+ // in order to do that we'd need some other way to track the return
214
+ // pointer during the initial render, like on the stack.)
215
+ //
216
+ // This case is always accompanied by a warning, but we still need to
217
+ // account for it. (There may be other cases that we haven't discovered,
218
+ // too.)
219
+ const offscreenInstance: OffscreenInstance | null = parent.stateNode;
220
+ if (offscreenInstance !== null && offscreenInstance.isHidden) {
221
+ isHidden = true;
222
}
223
}
224
+
225
node = parent;
226
parent = parent.return;
227
}
188
- if (node.tag === HostRoot) {
228
+
229
+ if (isHidden && update !== null && node.tag === HostRoot) {
230
const root: FiberRoot = node.stateNode;
190
- return root;
191
- } else {
192
- return null;
231
+ markHiddenUpdate(root, update, lane);
232
+ }
233
+}
234
+
235
+function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
236
+ // TODO: We will detect and infinite update loop and throw even if this fiber
237
+ // has already unmounted. This isn't really necessary but it happens to be the
238
+ // current behavior we've used for several release cycles. Consider not
239
+ // performing this check if the updated fiber already unmounted, since it's
240
+ // not possible for that to cause an infinite update loop.
241
+ throwIfInfiniteUpdateLoopDetected();
242
+
243
+ // When a setState happens, we must ensure the root is scheduled. Because
244
+ // update queues do not have a backpointer to the root, the only way to do
245
+ // this currently is to walk up the return path. This used to not be a big
246
+ // deal because we would have to walk up the return path to set
247
+ // the `childLanes`, anyway, but now those two traversals happen at
248
+ // different times.
249
+ // TODO: Consider adding a `root` backpointer on the update queue.
250
+ detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
251
+ let node = sourceFiber;
252
+ let parent = node.return;
253
+ while (parent !== null) {
254
+ detectUpdateOnUnmountedFiber(sourceFiber, node);
255
+ node = parent;
256
+ parent = node.return;
257
+ }
258
+ return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
259
+}
260
+
261
+function detectUpdateOnUnmountedFiber(sourceFiber: Fiber, parent: Fiber) {
262
+ if (__DEV__) {
263
+ const alternate = parent.alternate;
264
+ if (
265
+ alternate === null &&
266
+ (parent.flags & (Placement | Hydrating)) !== NoFlags
267
+ ) {
268
+ warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
269
+ }
270
}
271
}
packages/react-reconciler/src/ReactFiberHooks.old.js
+18
-28
@@ -43,6 +43,7 @@ import {
43
import {
44
NoLane,
45
SyncLane,
46
+ OffscreenLane,
47
NoLanes,
48
isSubsetOfLanes,
49
includesBlockingLane,
@@ -82,6 +83,7 @@ import {
83
} from './ReactHookEffectTags';
84
import {
85
getWorkInProgressRoot,
86
+ getWorkInProgressRootRenderLanes,
87
scheduleUpdateOnFiber,
88
requestUpdateLane,
89
requestEventTime,
@@ -130,7 +132,6 @@ export type Update<S, A> = {|
132
133
export type UpdateQueue<S, A> = {|
134
pending: Update<S, A> | null,
133
- interleaved: Update<S, A> | null,
135
lanes: Lanes,
136
dispatch: (A => mixed) | null,
137
lastRenderedReducer: ((S, A) => S) | null,
@@ -740,7 +741,6 @@ function mountReducer<S, I, A>(
741
hook.memoizedState = hook.baseState = initialState;
742
const queue: UpdateQueue<S, A> = {
743
pending: null,
743
- interleaved: null,
744
lanes: NoLanes,
745
dispatch: null,
746
lastRenderedReducer: reducer,
@@ -812,8 +812,20 @@ function updateReducer<S, I, A>(
812
let newBaseQueueLast = null;
813
let update = first;
814
do {
815
- const updateLane = update.lane;
816
- if (!isSubsetOfLanes(renderLanes, updateLane)) {
815
+ // An extra OffscreenLane bit is added to updates that were made to
816
+ // a hidden tree, so that we can distinguish them from updates that were
817
+ // already there when the tree was hidden.
818
+ const updateLane = removeLanes(update.lane, OffscreenLane);
819
+ const isHiddenUpdate = updateLane !== update.lane;
820
+
821
+ // Check if this update was made while the tree was hidden. If so, then
822
+ // it's not a "base" update and we should disregard the extra base lanes
823
+ // that were added to renderLanes when we entered the Offscreen tree.
824
+ const shouldSkipUpdate = isHiddenUpdate
825
+ ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
826
+ : !isSubsetOfLanes(renderLanes, updateLane);
827
+
828
+ if (shouldSkipUpdate) {
829
// Priority is insufficient. Skip this update. If this is the first
830
// skipped update, the previous update/state is the new base
831
// update/state.
@@ -887,22 +899,7 @@ function updateReducer<S, I, A>(
899
queue.lastRenderedState = newState;
900
}
901
890
- // Interleaved updates are stored on a separate queue. We aren't going to
891
- // process them during this render, but we do need to track which lanes
892
- // are remaining.
893
- const lastInterleaved = queue.interleaved;
894
- if (lastInterleaved !== null) {
895
- let interleaved = lastInterleaved;
896
- do {
897
- const interleavedLane = interleaved.lane;
898
- currentlyRenderingFiber.lanes = mergeLanes(
899
- currentlyRenderingFiber.lanes,
900
- interleavedLane,
901
- );
902
- markSkippedUpdateLanes(interleavedLane);
903
- interleaved = ((interleaved: any).next: Update<S, A>);
904
- } while (interleaved !== lastInterleaved);
905
- } else if (baseQueue === null) {
902
+ if (baseQueue === null) {
903
// `queue.lanes` is used for entangling transitions. We can set it back to
904
// zero once the queue is empty.
905
queue.lanes = NoLanes;
@@ -1210,7 +1207,6 @@ function useMutableSource<Source, Snapshot>(
1207
// including any interleaving updates that occur.
1208
const newQueue: UpdateQueue<Snapshot, BasicStateAction<Snapshot>> = {
1209
pending: null,
1213
- interleaved: null,
1210
lanes: NoLanes,
1211
dispatch: null,
1212
lastRenderedReducer: basicStateReducer,
@@ -1516,7 +1512,6 @@ function mountState<S>(
1512
hook.memoizedState = hook.baseState = initialState;
1513
const queue: UpdateQueue<S, BasicStateAction<S>> = {
1514
pending: null,
1519
- interleaved: null,
1515
lanes: NoLanes,
1516
dispatch: null,
1517
lastRenderedReducer: basicStateReducer,
@@ -2281,12 +2276,7 @@ function dispatchSetState<S, A>(
2276
// if the component re-renders for a different reason and by that
2277
// time the reducer has changed.
2278
// TODO: Do we still need to entangle transitions in this case?
2284
- enqueueConcurrentHookUpdateAndEagerlyBailout(
2285
- fiber,
2286
- queue,
2287
- update,
2288
- lane,
2289
- );
2279
+ enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
2280
return;
2281
}
2282
} catch (error) {
packages/react-reconciler/src/ReactFiberLane.old.js
+33
@@ -9,6 +9,7 @@
9
10
import type {FiberRoot} from './ReactInternalTypes';
11
import type {Transition} from './ReactFiberTracingMarkerComponent.old';
12
+import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates.old';
13
14
// TODO: Ideally these types would be opaque but that doesn't work well with
15
// our reconciler fork infra, since these leak into non-reconciler packages.
@@ -648,6 +649,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
649
const entanglements = root.entanglements;
650
const eventTimes = root.eventTimes;
651
const expirationTimes = root.expirationTimes;
652
+ const hiddenUpdates = root.hiddenUpdates;
653
654
// Clear the lanes that no longer have pending work
655
let lanes = noLongerPendingLanes;
@@ -659,6 +661,21 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
661
eventTimes[index] = NoTimestamp;
662
expirationTimes[index] = NoTimestamp;
663
664
+ const hiddenUpdatesForLane = hiddenUpdates[index];
665
+ if (hiddenUpdatesForLane !== null) {
666
+ hiddenUpdates[index] = null;
667
+ // "Hidden" updates are updates that were made to a hidden component. They
668
+ // have special logic associated with them because they may be entangled
669
+ // with updates that occur outside that tree. But once the outer tree
670
+ // commits, they behave like regular updates.
671
+ for (let i = 0; i < hiddenUpdatesForLane.length; i++) {
672
+ const update = hiddenUpdatesForLane[i];
673
+ if (update !== null) {
674
+ update.lane &= ~OffscreenLane;
675
+ }
676
+ }
677
+ }
678
+
679
lanes &= ~lane;
680
}
681
}
@@ -694,6 +711,22 @@ export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
711
}
712
}
713
714
+export function markHiddenUpdate(
715
+ root: FiberRoot,
716
+ update: ConcurrentUpdate,
717
+ lane: Lane,
718
+) {
719
+ const index = laneToIndex(lane);
720
+ const hiddenUpdates = root.hiddenUpdates;
721
+ const hiddenUpdatesForLane = hiddenUpdates[index];
722
+ if (hiddenUpdatesForLane === null) {
723
+ hiddenUpdates[index] = [update];
724
+ } else {
725
+ hiddenUpdatesForLane.push(update);
726
+ }
727
+ update.lane = lane | OffscreenLane;
728
+}
729
+
730
export function getBumpedLaneForHydration(
731
root: FiberRoot,
732
renderLanes: Lanes,
packages/react-reconciler/src/ReactFiberRoot.old.js
+2
@@ -74,6 +74,8 @@ function FiberRootNode(
74
this.entangledLanes = NoLanes;
75
this.entanglements = createLaneMap(NoLanes);
76
77
+ this.hiddenUpdates = createLaneMap(null);
78
+
79
this.identifierPrefix = identifierPrefix;
80
this.onRecoverableError = onRecoverableError;
81
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+15
-2
@@ -200,6 +200,7 @@ import {
200
import {
201
enqueueConcurrentRenderForLane,
202
finishQueueingConcurrentUpdates,
203
+ getConcurrentlyUpdatedLanes,
204
} from './ReactFiberConcurrentUpdates.old';
205
206
import {
@@ -448,6 +449,10 @@ export function getWorkInProgressRoot(): FiberRoot | null {
449
return workInProgressRoot;
450
}
451
452
+export function getWorkInProgressRootRenderLanes(): Lanes {
453
+ return workInProgressRootRenderLanes;
454
+}
455
+
456
export function requestEventTime() {
457
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
458
// We're inside React, so it's fine to read the actual time.
@@ -2074,9 +2079,15 @@ function commitRootImpl(
2079
root.callbackNode = null;
2080
root.callbackPriority = NoLane;
2081
2077
- // Update the first and last pending times on this root. The new first
2078
- // pending time is whatever is left on the root fiber.
2082
+ // Check which lanes no longer have any work scheduled on them, and mark
2083
+ // those as finished.
2084
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
2085
+
2086
+ // Make sure to account for lanes that were updated by a concurrent event
2087
+ // during the render phase; don't mark them as finished.
2088
+ const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
2089
+ remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
2090
+
2091
markRootFinished(root, remainingLanes);
2092
2093
if (root === workInProgressRoot) {
@@ -2795,7 +2806,9 @@ function jnd(timeElapsed: number) {
2806
export function throwIfInfiniteUpdateLoopDetected() {
2807
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
2808
nestedUpdateCount = 0;
2809
+ nestedPassiveUpdateCount = 0;
2810
rootWithNestedUpdates = null;
2811
+ rootWithPassiveNestedUpdates = null;
2812
2813
throw new Error(
2814
'Maximum update depth exceeded. This can happen when a component ' +
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+1
-2
@@ -474,8 +474,7 @@ describe('ReactOffscreen', () => {
474
expect(root).toMatchRenderedOutput(<span>Hi</span>);
475
});
476
477
- // Only works in new reconciler
478
- // @gate variant
477
+ // @gate experimental || www
478
it('revealing a hidden tree at high priority does not cause tearing', async () => {
479
// When revealing an offscreen tree, we need to include updates that were
480
// previously deferred because the tree was hidden, even if they are lower
packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
+2
-3
@@ -86,8 +86,7 @@ describe('ReactOffscreen', () => {
86
return text;
87
}
88
89
- // Only works in new reconciler
90
- // @gate variant
89
+ // @gate experimental || www
90
test('detect updates to a hidden tree during a concurrent event', async () => {
91
// This is a pretty complex test case. It relates to how we detect if an
92
// update is made to a hidden tree: when scheduling the update, we walk up
@@ -168,7 +167,7 @@ describe('ReactOffscreen', () => {
167
// In the same render, also hide the offscreen tree.
168
root.render(<App show={false} />);
169
171
- expect(Scheduler).toFlushAndYieldThrough([
170
+ expect(Scheduler).toFlushUntilNextPaint([
171
// The outer update will commit, but the inner update is deferred until
172
// a later render.
173
'Outer: 1',
scripts/merge-fork/forked-revisions
-4
@@ -1,4 +0,0 @@
1
-d410f0a1bbb12e972b0e99bb9faea10c7e62894d [FORKED] Bugfix: Offscreen instance is null during setState
2
-58bb11764bf0bb6db47527a64f693f67cdd3b0bb [FORKED] Check for infinite update loops even if unmounted
3
-31882b5dd66f34f70d341ea2781cacbe802bf4d5 [FORKED] Bugfix: Revealing a hidden update
4
-17691acc071d56261d43c3cf183f287d983baa9b [FORKED] Don't update childLanes until after current render