Skip to content

Commit

Permalink
Defer setState callbacks until component is visible (#24872)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Jul 8, 2022
1 parent 95e22ff commit 5e4e2da
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 78 deletions.
103 changes: 71 additions & 32 deletions packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js
Expand Up @@ -102,7 +102,12 @@ import {
enterDisallowedContextReadInDEV,
exitDisallowedContextReadInDEV,
} from './ReactFiberNewContext.new';
import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags';
import {
Callback,
Visibility,
ShouldCapture,
DidCapture,
} from './ReactFiberFlags';

import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';

Expand Down Expand Up @@ -136,14 +141,15 @@ export type Update<State> = {|
export type SharedQueue<State> = {|
pending: Update<State> | null,
lanes: Lanes,
hiddenCallbacks: Array<() => mixed> | null,
|};

export type UpdateQueue<State> = {|
baseState: State,
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
effects: Array<Update<State>> | null,
callbacks: Array<() => mixed> | null,
|};

export const UpdateState = 0;
Expand Down Expand Up @@ -175,8 +181,9 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
effects: null,
callbacks: null,
};
fiber.updateQueue = queue;
}
Expand All @@ -194,7 +201,7 @@ export function cloneUpdateQueue<State>(
firstBaseUpdate: currentQueue.firstBaseUpdate,
lastBaseUpdate: currentQueue.lastBaseUpdate,
shared: currentQueue.shared,
effects: currentQueue.effects,
callbacks: null,
};
workInProgress.updateQueue = clone;
}
Expand Down Expand Up @@ -326,7 +333,9 @@ export function enqueueCapturedUpdate<State>(

tag: update.tag,
payload: update.payload,
callback: update.callback,
// When this update is rebased, we should not fire its
// callback again.
callback: null,

next: null,
};
Expand Down Expand Up @@ -355,7 +364,7 @@ export function enqueueCapturedUpdate<State>(
firstBaseUpdate: newFirst,
lastBaseUpdate: newLast,
shared: currentQueue.shared,
effects: currentQueue.effects,
callbacks: currentQueue.callbacks,
};
workInProgress.updateQueue = queue;
return;
Expand Down Expand Up @@ -577,7 +586,10 @@ export function processUpdateQueue<State>(

tag: update.tag,
payload: update.payload,
callback: update.callback,

// When this update is rebased, we should not fire its
// callback again.
callback: null,

next: null,
};
Expand All @@ -594,18 +606,16 @@ export function processUpdateQueue<State>(
instance,
);
const callback = update.callback;
if (
callback !== null &&
// If the update was already committed, we should not queue its
// callback again.
update.lane !== NoLane
) {
if (callback !== null) {
workInProgress.flags |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
if (isHiddenUpdate) {
workInProgress.flags |= Visibility;
}
const callbacks = queue.callbacks;
if (callbacks === null) {
queue.callbacks = [callback];
} else {
effects.push(update);
callbacks.push(callback);
}
}
}
Expand Down Expand Up @@ -679,22 +689,51 @@ export function checkHasForceUpdateAfterProcessing(): boolean {
return hasForceUpdate;
}

export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
export function deferHiddenCallbacks<State>(
updateQueue: UpdateQueue<State>,
): void {
// Commit the effects
const effects = finishedQueue.effects;
finishedQueue.effects = null;
if (effects !== null) {
for (let i = 0; i < effects.length; i++) {
const effect = effects[i];
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
// When an update finishes on a hidden component, its callback should not
// be fired until/unless the component is made visible again. Stash the
// callback on the shared queue object so it can be fired later.
const newHiddenCallbacks = updateQueue.callbacks;
if (newHiddenCallbacks !== null) {
const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;
if (existingHiddenCallbacks === null) {
updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;
} else {
updateQueue.shared.hiddenCallbacks = existingHiddenCallbacks.concat(
newHiddenCallbacks,
);
}
}
}

export function commitHiddenCallbacks<State>(
updateQueue: UpdateQueue<State>,
context: any,
): void {
// This component is switching from hidden -> visible. Commit any callbacks
// that were previously deferred.
const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;
if (hiddenCallbacks !== null) {
updateQueue.shared.hiddenCallbacks = null;
for (let i = 0; i < hiddenCallbacks.length; i++) {
const callback = hiddenCallbacks[i];
callCallback(callback, context);
}
}
}

export function commitCallbacks<State>(
updateQueue: UpdateQueue<State>,
context: any,
): void {
const callbacks = updateQueue.callbacks;
if (callbacks !== null) {
updateQueue.callbacks = null;
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i];
callCallback(callback, context);
}
}
}
103 changes: 71 additions & 32 deletions packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js
Expand Up @@ -102,7 +102,12 @@ import {
enterDisallowedContextReadInDEV,
exitDisallowedContextReadInDEV,
} from './ReactFiberNewContext.old';
import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags';
import {
Callback,
Visibility,
ShouldCapture,
DidCapture,
} from './ReactFiberFlags';

import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';

Expand Down Expand Up @@ -136,14 +141,15 @@ export type Update<State> = {|
export type SharedQueue<State> = {|
pending: Update<State> | null,
lanes: Lanes,
hiddenCallbacks: Array<() => mixed> | null,
|};

export type UpdateQueue<State> = {|
baseState: State,
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
effects: Array<Update<State>> | null,
callbacks: Array<() => mixed> | null,
|};

export const UpdateState = 0;
Expand Down Expand Up @@ -175,8 +181,9 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
effects: null,
callbacks: null,
};
fiber.updateQueue = queue;
}
Expand All @@ -194,7 +201,7 @@ export function cloneUpdateQueue<State>(
firstBaseUpdate: currentQueue.firstBaseUpdate,
lastBaseUpdate: currentQueue.lastBaseUpdate,
shared: currentQueue.shared,
effects: currentQueue.effects,
callbacks: null,
};
workInProgress.updateQueue = clone;
}
Expand Down Expand Up @@ -326,7 +333,9 @@ export function enqueueCapturedUpdate<State>(

tag: update.tag,
payload: update.payload,
callback: update.callback,
// When this update is rebased, we should not fire its
// callback again.
callback: null,

next: null,
};
Expand Down Expand Up @@ -355,7 +364,7 @@ export function enqueueCapturedUpdate<State>(
firstBaseUpdate: newFirst,
lastBaseUpdate: newLast,
shared: currentQueue.shared,
effects: currentQueue.effects,
callbacks: currentQueue.callbacks,
};
workInProgress.updateQueue = queue;
return;
Expand Down Expand Up @@ -577,7 +586,10 @@ export function processUpdateQueue<State>(

tag: update.tag,
payload: update.payload,
callback: update.callback,

// When this update is rebased, we should not fire its
// callback again.
callback: null,

next: null,
};
Expand All @@ -594,18 +606,16 @@ export function processUpdateQueue<State>(
instance,
);
const callback = update.callback;
if (
callback !== null &&
// If the update was already committed, we should not queue its
// callback again.
update.lane !== NoLane
) {
if (callback !== null) {
workInProgress.flags |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
if (isHiddenUpdate) {
workInProgress.flags |= Visibility;
}
const callbacks = queue.callbacks;
if (callbacks === null) {
queue.callbacks = [callback];
} else {
effects.push(update);
callbacks.push(callback);
}
}
}
Expand Down Expand Up @@ -679,22 +689,51 @@ export function checkHasForceUpdateAfterProcessing(): boolean {
return hasForceUpdate;
}

export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
export function deferHiddenCallbacks<State>(
updateQueue: UpdateQueue<State>,
): void {
// Commit the effects
const effects = finishedQueue.effects;
finishedQueue.effects = null;
if (effects !== null) {
for (let i = 0; i < effects.length; i++) {
const effect = effects[i];
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
// When an update finishes on a hidden component, its callback should not
// be fired until/unless the component is made visible again. Stash the
// callback on the shared queue object so it can be fired later.
const newHiddenCallbacks = updateQueue.callbacks;
if (newHiddenCallbacks !== null) {
const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;
if (existingHiddenCallbacks === null) {
updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;
} else {
updateQueue.shared.hiddenCallbacks = existingHiddenCallbacks.concat(
newHiddenCallbacks,
);
}
}
}

export function commitHiddenCallbacks<State>(
updateQueue: UpdateQueue<State>,
context: any,
): void {
// This component is switching from hidden -> visible. Commit any callbacks
// that were previously deferred.
const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;
if (hiddenCallbacks !== null) {
updateQueue.shared.hiddenCallbacks = null;
for (let i = 0; i < hiddenCallbacks.length; i++) {
const callback = hiddenCallbacks[i];
callCallback(callback, context);
}
}
}

export function commitCallbacks<State>(
updateQueue: UpdateQueue<State>,
context: any,
): void {
const callbacks = updateQueue.callbacks;
if (callbacks !== null) {
updateQueue.callbacks = null;
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i];
callCallback(callback, context);
}
}
}

0 comments on commit 5e4e2da

Please sign in to comment.