Skip to content

Commit 5e4e2da

Browse files
authored
Defer setState callbacks until component is visible (#24872)
A class component `setState` callback should not fire if a component is inside a hidden Offscreen tree. Instead, it should wait until the next time the component is made visible.
1 parent 95e22ff commit 5e4e2da

File tree

5 files changed

+254
-78
lines changed

5 files changed

+254
-78
lines changed

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

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ import {
102102
enterDisallowedContextReadInDEV,
103103
exitDisallowedContextReadInDEV,
104104
} from './ReactFiberNewContext.new';
105-
import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags';
105+
import {
106+
Callback,
107+
Visibility,
108+
ShouldCapture,
109+
DidCapture,
110+
} from './ReactFiberFlags';
106111

107112
import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';
108113

@@ -136,14 +141,15 @@ export type Update<State> = {|
136141
export type SharedQueue<State> = {|
137142
pending: Update<State> | null,
138143
lanes: Lanes,
144+
hiddenCallbacks: Array<() => mixed> | null,
139145
|};
140146

141147
export type UpdateQueue<State> = {|
142148
baseState: State,
143149
firstBaseUpdate: Update<State> | null,
144150
lastBaseUpdate: Update<State> | null,
145151
shared: SharedQueue<State>,
146-
effects: Array<Update<State>> | null,
152+
callbacks: Array<() => mixed> | null,
147153
|};
148154

149155
export const UpdateState = 0;
@@ -175,8 +181,9 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
175181
shared: {
176182
pending: null,
177183
lanes: NoLanes,
184+
hiddenCallbacks: null,
178185
},
179-
effects: null,
186+
callbacks: null,
180187
};
181188
fiber.updateQueue = queue;
182189
}
@@ -194,7 +201,7 @@ export function cloneUpdateQueue<State>(
194201
firstBaseUpdate: currentQueue.firstBaseUpdate,
195202
lastBaseUpdate: currentQueue.lastBaseUpdate,
196203
shared: currentQueue.shared,
197-
effects: currentQueue.effects,
204+
callbacks: null,
198205
};
199206
workInProgress.updateQueue = clone;
200207
}
@@ -326,7 +333,9 @@ export function enqueueCapturedUpdate<State>(
326333

327334
tag: update.tag,
328335
payload: update.payload,
329-
callback: update.callback,
336+
// When this update is rebased, we should not fire its
337+
// callback again.
338+
callback: null,
330339

331340
next: null,
332341
};
@@ -355,7 +364,7 @@ export function enqueueCapturedUpdate<State>(
355364
firstBaseUpdate: newFirst,
356365
lastBaseUpdate: newLast,
357366
shared: currentQueue.shared,
358-
effects: currentQueue.effects,
367+
callbacks: currentQueue.callbacks,
359368
};
360369
workInProgress.updateQueue = queue;
361370
return;
@@ -577,7 +586,10 @@ export function processUpdateQueue<State>(
577586

578587
tag: update.tag,
579588
payload: update.payload,
580-
callback: update.callback,
589+
590+
// When this update is rebased, we should not fire its
591+
// callback again.
592+
callback: null,
581593

582594
next: null,
583595
};
@@ -594,18 +606,16 @@ export function processUpdateQueue<State>(
594606
instance,
595607
);
596608
const callback = update.callback;
597-
if (
598-
callback !== null &&
599-
// If the update was already committed, we should not queue its
600-
// callback again.
601-
update.lane !== NoLane
602-
) {
609+
if (callback !== null) {
603610
workInProgress.flags |= Callback;
604-
const effects = queue.effects;
605-
if (effects === null) {
606-
queue.effects = [update];
611+
if (isHiddenUpdate) {
612+
workInProgress.flags |= Visibility;
613+
}
614+
const callbacks = queue.callbacks;
615+
if (callbacks === null) {
616+
queue.callbacks = [callback];
607617
} else {
608-
effects.push(update);
618+
callbacks.push(callback);
609619
}
610620
}
611621
}
@@ -679,22 +689,51 @@ export function checkHasForceUpdateAfterProcessing(): boolean {
679689
return hasForceUpdate;
680690
}
681691

682-
export function commitUpdateQueue<State>(
683-
finishedWork: Fiber,
684-
finishedQueue: UpdateQueue<State>,
685-
instance: any,
692+
export function deferHiddenCallbacks<State>(
693+
updateQueue: UpdateQueue<State>,
686694
): void {
687-
// Commit the effects
688-
const effects = finishedQueue.effects;
689-
finishedQueue.effects = null;
690-
if (effects !== null) {
691-
for (let i = 0; i < effects.length; i++) {
692-
const effect = effects[i];
693-
const callback = effect.callback;
694-
if (callback !== null) {
695-
effect.callback = null;
696-
callCallback(callback, instance);
697-
}
695+
// When an update finishes on a hidden component, its callback should not
696+
// be fired until/unless the component is made visible again. Stash the
697+
// callback on the shared queue object so it can be fired later.
698+
const newHiddenCallbacks = updateQueue.callbacks;
699+
if (newHiddenCallbacks !== null) {
700+
const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;
701+
if (existingHiddenCallbacks === null) {
702+
updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;
703+
} else {
704+
updateQueue.shared.hiddenCallbacks = existingHiddenCallbacks.concat(
705+
newHiddenCallbacks,
706+
);
707+
}
708+
}
709+
}
710+
711+
export function commitHiddenCallbacks<State>(
712+
updateQueue: UpdateQueue<State>,
713+
context: any,
714+
): void {
715+
// This component is switching from hidden -> visible. Commit any callbacks
716+
// that were previously deferred.
717+
const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;
718+
if (hiddenCallbacks !== null) {
719+
updateQueue.shared.hiddenCallbacks = null;
720+
for (let i = 0; i < hiddenCallbacks.length; i++) {
721+
const callback = hiddenCallbacks[i];
722+
callCallback(callback, context);
723+
}
724+
}
725+
}
726+
727+
export function commitCallbacks<State>(
728+
updateQueue: UpdateQueue<State>,
729+
context: any,
730+
): void {
731+
const callbacks = updateQueue.callbacks;
732+
if (callbacks !== null) {
733+
updateQueue.callbacks = null;
734+
for (let i = 0; i < callbacks.length; i++) {
735+
const callback = callbacks[i];
736+
callCallback(callback, context);
698737
}
699738
}
700739
}

packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ import {
102102
enterDisallowedContextReadInDEV,
103103
exitDisallowedContextReadInDEV,
104104
} from './ReactFiberNewContext.old';
105-
import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags';
105+
import {
106+
Callback,
107+
Visibility,
108+
ShouldCapture,
109+
DidCapture,
110+
} from './ReactFiberFlags';
106111

107112
import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';
108113

@@ -136,14 +141,15 @@ export type Update<State> = {|
136141
export type SharedQueue<State> = {|
137142
pending: Update<State> | null,
138143
lanes: Lanes,
144+
hiddenCallbacks: Array<() => mixed> | null,
139145
|};
140146

141147
export type UpdateQueue<State> = {|
142148
baseState: State,
143149
firstBaseUpdate: Update<State> | null,
144150
lastBaseUpdate: Update<State> | null,
145151
shared: SharedQueue<State>,
146-
effects: Array<Update<State>> | null,
152+
callbacks: Array<() => mixed> | null,
147153
|};
148154

149155
export const UpdateState = 0;
@@ -175,8 +181,9 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
175181
shared: {
176182
pending: null,
177183
lanes: NoLanes,
184+
hiddenCallbacks: null,
178185
},
179-
effects: null,
186+
callbacks: null,
180187
};
181188
fiber.updateQueue = queue;
182189
}
@@ -194,7 +201,7 @@ export function cloneUpdateQueue<State>(
194201
firstBaseUpdate: currentQueue.firstBaseUpdate,
195202
lastBaseUpdate: currentQueue.lastBaseUpdate,
196203
shared: currentQueue.shared,
197-
effects: currentQueue.effects,
204+
callbacks: null,
198205
};
199206
workInProgress.updateQueue = clone;
200207
}
@@ -326,7 +333,9 @@ export function enqueueCapturedUpdate<State>(
326333

327334
tag: update.tag,
328335
payload: update.payload,
329-
callback: update.callback,
336+
// When this update is rebased, we should not fire its
337+
// callback again.
338+
callback: null,
330339

331340
next: null,
332341
};
@@ -355,7 +364,7 @@ export function enqueueCapturedUpdate<State>(
355364
firstBaseUpdate: newFirst,
356365
lastBaseUpdate: newLast,
357366
shared: currentQueue.shared,
358-
effects: currentQueue.effects,
367+
callbacks: currentQueue.callbacks,
359368
};
360369
workInProgress.updateQueue = queue;
361370
return;
@@ -577,7 +586,10 @@ export function processUpdateQueue<State>(
577586

578587
tag: update.tag,
579588
payload: update.payload,
580-
callback: update.callback,
589+
590+
// When this update is rebased, we should not fire its
591+
// callback again.
592+
callback: null,
581593

582594
next: null,
583595
};
@@ -594,18 +606,16 @@ export function processUpdateQueue<State>(
594606
instance,
595607
);
596608
const callback = update.callback;
597-
if (
598-
callback !== null &&
599-
// If the update was already committed, we should not queue its
600-
// callback again.
601-
update.lane !== NoLane
602-
) {
609+
if (callback !== null) {
603610
workInProgress.flags |= Callback;
604-
const effects = queue.effects;
605-
if (effects === null) {
606-
queue.effects = [update];
611+
if (isHiddenUpdate) {
612+
workInProgress.flags |= Visibility;
613+
}
614+
const callbacks = queue.callbacks;
615+
if (callbacks === null) {
616+
queue.callbacks = [callback];
607617
} else {
608-
effects.push(update);
618+
callbacks.push(callback);
609619
}
610620
}
611621
}
@@ -679,22 +689,51 @@ export function checkHasForceUpdateAfterProcessing(): boolean {
679689
return hasForceUpdate;
680690
}
681691

682-
export function commitUpdateQueue<State>(
683-
finishedWork: Fiber,
684-
finishedQueue: UpdateQueue<State>,
685-
instance: any,
692+
export function deferHiddenCallbacks<State>(
693+
updateQueue: UpdateQueue<State>,
686694
): void {
687-
// Commit the effects
688-
const effects = finishedQueue.effects;
689-
finishedQueue.effects = null;
690-
if (effects !== null) {
691-
for (let i = 0; i < effects.length; i++) {
692-
const effect = effects[i];
693-
const callback = effect.callback;
694-
if (callback !== null) {
695-
effect.callback = null;
696-
callCallback(callback, instance);
697-
}
695+
// When an update finishes on a hidden component, its callback should not
696+
// be fired until/unless the component is made visible again. Stash the
697+
// callback on the shared queue object so it can be fired later.
698+
const newHiddenCallbacks = updateQueue.callbacks;
699+
if (newHiddenCallbacks !== null) {
700+
const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;
701+
if (existingHiddenCallbacks === null) {
702+
updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;
703+
} else {
704+
updateQueue.shared.hiddenCallbacks = existingHiddenCallbacks.concat(
705+
newHiddenCallbacks,
706+
);
707+
}
708+
}
709+
}
710+
711+
export function commitHiddenCallbacks<State>(
712+
updateQueue: UpdateQueue<State>,
713+
context: any,
714+
): void {
715+
// This component is switching from hidden -> visible. Commit any callbacks
716+
// that were previously deferred.
717+
const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;
718+
if (hiddenCallbacks !== null) {
719+
updateQueue.shared.hiddenCallbacks = null;
720+
for (let i = 0; i < hiddenCallbacks.length; i++) {
721+
const callback = hiddenCallbacks[i];
722+
callCallback(callback, context);
723+
}
724+
}
725+
}
726+
727+
export function commitCallbacks<State>(
728+
updateQueue: UpdateQueue<State>,
729+
context: any,
730+
): void {
731+
const callbacks = updateQueue.callbacks;
732+
if (callbacks !== null) {
733+
updateQueue.callbacks = null;
734+
for (let i = 0; i < callbacks.length; i++) {
735+
const callback = callbacks[i];
736+
callCallback(callback, context);
698737
}
699738
}
700739
}

0 commit comments

Comments
 (0)