Skip to content

Commit 14072ce

Browse files
authored
Add detach to Offscreen component (#25265)
1 parent 3bb71df commit 14072ce

13 files changed

+350
-17
lines changed

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ import {
7373
} from './ReactWorkTags';
7474
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
7575
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
76-
7776
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
7877
import {
7978
resolveClassForHotReloading,
@@ -109,6 +108,7 @@ import {
109108
REACT_TRACING_MARKER_TYPE,
110109
} from 'shared/ReactSymbols';
111110
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.new';
111+
import {detachOffscreenInstance} from './ReactFiberCommitWork.new';
112112

113113
export type {Fiber};
114114

@@ -755,6 +755,8 @@ export function createFiberFromOffscreen(
755755
_pendingMarkers: null,
756756
_retryCache: null,
757757
_transitions: null,
758+
_current: null,
759+
detach: () => detachOffscreenInstance(primaryChildInstance),
758760
};
759761
fiber.stateNode = primaryChildInstance;
760762
return fiber;
@@ -776,6 +778,8 @@ export function createFiberFromLegacyHidden(
776778
_pendingMarkers: null,
777779
_transitions: null,
778780
_retryCache: null,
781+
_current: null,
782+
detach: () => detachOffscreenInstance(instance),
779783
};
780784
fiber.stateNode = instance;
781785
return fiber;

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ import {
7373
} from './ReactWorkTags';
7474
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
7575
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
76-
7776
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
7877
import {
7978
resolveClassForHotReloading,
@@ -109,6 +108,7 @@ import {
109108
REACT_TRACING_MARKER_TYPE,
110109
} from 'shared/ReactSymbols';
111110
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent.old';
111+
import {detachOffscreenInstance} from './ReactFiberCommitWork.old';
112112

113113
export type {Fiber};
114114

@@ -755,6 +755,8 @@ export function createFiberFromOffscreen(
755755
_pendingMarkers: null,
756756
_retryCache: null,
757757
_transitions: null,
758+
_current: null,
759+
detach: () => detachOffscreenInstance(primaryChildInstance),
758760
};
759761
fiber.stateNode = primaryChildInstance;
760762
return fiber;
@@ -776,6 +778,8 @@ export function createFiberFromLegacyHidden(
776778
_pendingMarkers: null,
777779
_transitions: null,
778780
_retryCache: null,
781+
_current: null,
782+
detach: () => detachOffscreenInstance(instance),
779783
};
780784
fiber.stateNode = instance;
781785
return fiber;

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
OffscreenQueue,
3030
OffscreenInstance,
3131
} from './ReactFiberOffscreenComponent';
32+
import {OffscreenDetached} from './ReactFiberOffscreenComponent';
3233
import type {
3334
Cache,
3435
CacheComponentState,
@@ -37,7 +38,6 @@ import type {
3738
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
3839
import type {RootState} from './ReactFiberRoot.new';
3940
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
40-
4141
import checkPropTypes from 'shared/checkPropTypes';
4242
import {
4343
markComponentRenderStarted,
@@ -688,7 +688,10 @@ function updateOffscreenComponent(
688688

689689
if (
690690
nextProps.mode === 'hidden' ||
691-
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
691+
(enableLegacyHidden &&
692+
nextProps.mode === 'unstable-defer-without-hiding') ||
693+
// TODO: remove read from stateNode.
694+
workInProgress.stateNode._visibility & OffscreenDetached
692695
) {
693696
// Rendering a hidden tree.
694697

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
OffscreenQueue,
3030
OffscreenInstance,
3131
} from './ReactFiberOffscreenComponent';
32+
import {OffscreenDetached} from './ReactFiberOffscreenComponent';
3233
import type {
3334
Cache,
3435
CacheComponentState,
@@ -37,7 +38,6 @@ import type {
3738
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
3839
import type {RootState} from './ReactFiberRoot.old';
3940
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
40-
4141
import checkPropTypes from 'shared/checkPropTypes';
4242
import {
4343
markComponentRenderStarted,
@@ -688,7 +688,10 @@ function updateOffscreenComponent(
688688

689689
if (
690690
nextProps.mode === 'hidden' ||
691-
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
691+
(enableLegacyHidden &&
692+
nextProps.mode === 'unstable-defer-without-hiding') ||
693+
// TODO: remove read from stateNode.
694+
workInProgress.stateNode._visibility & OffscreenDetached
692695
) {
693696
// Rendering a hidden tree.
694697

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
2525
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
2626
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
2727
import type {Wakeable} from 'shared/ReactTypes';
28+
import {isOffscreenManual} from './ReactFiberOffscreenComponent';
2829
import type {
2930
OffscreenState,
3031
OffscreenInstance,
@@ -156,6 +157,7 @@ import {
156157
clearSingleton,
157158
acquireSingletonInstance,
158159
releaseSingletonInstance,
160+
scheduleMicrotask,
159161
} from './ReactFiberHostConfig';
160162
import {
161163
captureCommitPhaseError,
@@ -172,6 +174,7 @@ import {
172174
setIsRunningInsertionEffect,
173175
getExecutionContext,
174176
CommitContext,
177+
RenderContext,
175178
NoContext,
176179
} from './ReactFiberWorkLoop.new';
177180
import {
@@ -200,6 +203,7 @@ import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
200203
import {clearTransitionsForLanes} from './ReactFiberLane.new';
201204
import {
202205
OffscreenVisible,
206+
OffscreenDetached,
203207
OffscreenPassiveEffectsConnected,
204208
} from './ReactFiberOffscreenComponent';
205209
import {
@@ -2416,6 +2420,28 @@ function getRetryCache(finishedWork) {
24162420
}
24172421
}
24182422

2423+
export function detachOffscreenInstance(instance: OffscreenInstance): void {
2424+
const currentOffscreenFiber = instance._current;
2425+
if (currentOffscreenFiber === null) {
2426+
throw new Error(
2427+
'Calling Offscreen.detach before instance handle has been set.',
2428+
);
2429+
}
2430+
2431+
const executionContext = getExecutionContext();
2432+
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
2433+
scheduleMicrotask(() => {
2434+
instance._visibility |= OffscreenDetached;
2435+
disappearLayoutEffects(currentOffscreenFiber);
2436+
disconnectPassiveEffect(currentOffscreenFiber);
2437+
});
2438+
} else {
2439+
instance._visibility |= OffscreenDetached;
2440+
disappearLayoutEffects(currentOffscreenFiber);
2441+
disconnectPassiveEffect(currentOffscreenFiber);
2442+
}
2443+
}
2444+
24192445
function attachSuspenseRetryListeners(
24202446
finishedWork: Fiber,
24212447
wakeables: Set<Wakeable>,
@@ -2845,6 +2871,8 @@ function commitMutationEffectsOnFiber(
28452871
}
28462872

28472873
commitReconciliationEffects(finishedWork);
2874+
// TODO: Add explicit effect flag to set _current.
2875+
finishedWork.stateNode._current = finishedWork;
28482876

28492877
if (flags & Visibility) {
28502878
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
@@ -2871,7 +2899,8 @@ function commitMutationEffectsOnFiber(
28712899
}
28722900
}
28732901

2874-
if (supportsMutation) {
2902+
// Offscreen with manual mode manages visibility manually.
2903+
if (supportsMutation && !isOffscreenManual(finishedWork)) {
28752904
// TODO: This needs to run whenever there's an insertion or update
28762905
// inside a hidden Offscreen tree.
28772906
hideOrUnhideAllChildren(offscreenBoundary, isHidden);

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
2525
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
2626
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
2727
import type {Wakeable} from 'shared/ReactTypes';
28+
import {isOffscreenManual} from './ReactFiberOffscreenComponent';
2829
import type {
2930
OffscreenState,
3031
OffscreenInstance,
@@ -156,6 +157,7 @@ import {
156157
clearSingleton,
157158
acquireSingletonInstance,
158159
releaseSingletonInstance,
160+
scheduleMicrotask,
159161
} from './ReactFiberHostConfig';
160162
import {
161163
captureCommitPhaseError,
@@ -172,6 +174,7 @@ import {
172174
setIsRunningInsertionEffect,
173175
getExecutionContext,
174176
CommitContext,
177+
RenderContext,
175178
NoContext,
176179
} from './ReactFiberWorkLoop.old';
177180
import {
@@ -200,6 +203,7 @@ import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
200203
import {clearTransitionsForLanes} from './ReactFiberLane.old';
201204
import {
202205
OffscreenVisible,
206+
OffscreenDetached,
203207
OffscreenPassiveEffectsConnected,
204208
} from './ReactFiberOffscreenComponent';
205209
import {
@@ -2416,6 +2420,28 @@ function getRetryCache(finishedWork) {
24162420
}
24172421
}
24182422

2423+
export function detachOffscreenInstance(instance: OffscreenInstance): void {
2424+
const currentOffscreenFiber = instance._current;
2425+
if (currentOffscreenFiber === null) {
2426+
throw new Error(
2427+
'Calling Offscreen.detach before instance handle has been set.',
2428+
);
2429+
}
2430+
2431+
const executionContext = getExecutionContext();
2432+
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
2433+
scheduleMicrotask(() => {
2434+
instance._visibility |= OffscreenDetached;
2435+
disappearLayoutEffects(currentOffscreenFiber);
2436+
disconnectPassiveEffect(currentOffscreenFiber);
2437+
});
2438+
} else {
2439+
instance._visibility |= OffscreenDetached;
2440+
disappearLayoutEffects(currentOffscreenFiber);
2441+
disconnectPassiveEffect(currentOffscreenFiber);
2442+
}
2443+
}
2444+
24192445
function attachSuspenseRetryListeners(
24202446
finishedWork: Fiber,
24212447
wakeables: Set<Wakeable>,
@@ -2845,6 +2871,8 @@ function commitMutationEffectsOnFiber(
28452871
}
28462872

28472873
commitReconciliationEffects(finishedWork);
2874+
// TODO: Add explicit effect flag to set _current.
2875+
finishedWork.stateNode._current = finishedWork;
28482876

28492877
if (flags & Visibility) {
28502878
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
@@ -2871,7 +2899,8 @@ function commitMutationEffectsOnFiber(
28712899
}
28722900
}
28732901

2874-
if (supportsMutation) {
2902+
// Offscreen with manual mode manages visibility manually.
2903+
if (supportsMutation && !isOffscreenManual(finishedWork)) {
28752904
// TODO: This needs to run whenever there's an insertion or update
28762905
// inside a hidden Offscreen tree.
28772906
hideOrUnhideAllChildren(offscreenBoundary, isHidden);

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
SuspenseState,
2727
SuspenseListRenderState,
2828
} from './ReactFiberSuspenseComponent.new';
29+
import {isOffscreenManual} from './ReactFiberOffscreenComponent';
2930
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3031
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
3132
import type {Cache} from './ReactFiberCacheComponent.new';
@@ -425,7 +426,14 @@ if (supportsMutation) {
425426
if (child !== null) {
426427
child.return = node;
427428
}
428-
appendAllChildrenToContainer(containerChildSet, node, true, true);
429+
// If Offscreen is not in manual mode, detached tree is hidden from user space.
430+
const _needsVisibilityToggle = !isOffscreenManual(node);
431+
appendAllChildrenToContainer(
432+
containerChildSet,
433+
node,
434+
_needsVisibilityToggle,
435+
true,
436+
);
429437
} else if (node.child !== null) {
430438
node.child.return = node;
431439
node = node.child;

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
SuspenseState,
2727
SuspenseListRenderState,
2828
} from './ReactFiberSuspenseComponent.old';
29+
import {isOffscreenManual} from './ReactFiberOffscreenComponent';
2930
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3031
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
3132
import type {Cache} from './ReactFiberCacheComponent.old';
@@ -425,7 +426,14 @@ if (supportsMutation) {
425426
if (child !== null) {
426427
child.return = node;
427428
}
428-
appendAllChildrenToContainer(containerChildSet, node, true, true);
429+
// If Offscreen is not in manual mode, detached tree is hidden from user space.
430+
const _needsVisibilityToggle = !isOffscreenManual(node);
431+
appendAllChildrenToContainer(
432+
containerChildSet,
433+
node,
434+
_needsVisibilityToggle,
435+
true,
436+
);
429437
} else if (node.child !== null) {
430438
node.child.return = node;
431439
node = node.child;

packages/react-reconciler/src/ReactFiberOffscreenComponent.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import type {ReactNodeList, OffscreenMode, Wakeable} from 'shared/ReactTypes';
1111
import type {Lanes} from './ReactFiberLane.old';
1212
import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
13+
import type {Fiber} from './ReactInternalTypes';
1314
import type {
1415
Transition,
1516
TracingMarkerInstance,
@@ -44,13 +45,27 @@ export type OffscreenQueue = {
4445

4546
type OffscreenVisibility = number;
4647

47-
export const OffscreenVisible = /* */ 0b01;
48-
export const OffscreenPassiveEffectsConnected = /* */ 0b10;
48+
export const OffscreenVisible = /* */ 0b001;
49+
export const OffscreenDetached = /* */ 0b010;
50+
export const OffscreenPassiveEffectsConnected = /* */ 0b100;
4951

5052
export type OffscreenInstance = {
5153
_visibility: OffscreenVisibility,
5254
_pendingMarkers: Set<TracingMarkerInstance> | null,
5355
_transitions: Set<Transition> | null,
5456
// $FlowFixMe[incompatible-type-arg] found when upgrading Flow
5557
_retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
58+
59+
// Represents the current Offscreen fiber
60+
_current: Fiber | null,
61+
detach: () => void,
62+
63+
// TODO: attach
5664
};
65+
66+
export function isOffscreenManual(offscreenFiber: Fiber): boolean {
67+
return (
68+
offscreenFiber.memoizedProps !== null &&
69+
offscreenFiber.memoizedProps.mode === 'manual'
70+
);
71+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ type ExecutionContext = number;
290290

291291
export const NoContext = /* */ 0b000;
292292
const BatchedContext = /* */ 0b001;
293-
const RenderContext = /* */ 0b010;
293+
export const RenderContext = /* */ 0b010;
294294
export const CommitContext = /* */ 0b100;
295295

296296
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ type ExecutionContext = number;
290290

291291
export const NoContext = /* */ 0b000;
292292
const BatchedContext = /* */ 0b001;
293-
const RenderContext = /* */ 0b010;
293+
export const RenderContext = /* */ 0b010;
294294
export const CommitContext = /* */ 0b100;
295295

296296
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;

0 commit comments

Comments
 (0)