@samitouri / QOS-React-2 / commits / 6b6cf8311c

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