Skip to content

Commit 996e4c0

Browse files
sammy-SCacdlite
andauthored
Offscreen add attach (#25603)
`Offscreen.attach` is imperative API to signal to Offscreen that its updates should be high priority and effects should be mounted. Coupled with `Offscreen.detach` it gives ability to manually control Offscreen. Unlike with mode `visible` and `hidden`, it is developers job to make sure contents of Offscreen are not visible to users. `Offscreen.attach` only works if mode is `manual`. Example uses: ```jsx let offscreenRef = useRef(null); <Offscreen mode={'manual'} ref={offscreenRef)}> <Child /> </Offscreen> // ------ // Offscreen is attached by default. // For example user scrolls away and Offscreen subtree is not visible anymore. offscreenRef.current.detach(); // User scrolls back and Offscreen subtree is visible again. offscreenRef.current.attach(); ``` Co-authored-by: Andrew Clark <[email protected]>
1 parent b14d7fa commit 996e4c0

File tree

5 files changed

+372
-43
lines changed

5 files changed

+372
-43
lines changed

packages/react-reconciler/src/ReactFiber.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ import {
107107
REACT_TRACING_MARKER_TYPE,
108108
} from 'shared/ReactSymbols';
109109
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
110-
import {detachOffscreenInstance} from './ReactFiberCommitWork';
110+
import {
111+
detachOffscreenInstance,
112+
attachOffscreenInstance,
113+
} from './ReactFiberCommitWork';
111114
import {getHostContext} from './ReactFiberHostContext';
112115

113116
export type {Fiber};
@@ -750,11 +753,13 @@ export function createFiberFromOffscreen(
750753
fiber.lanes = lanes;
751754
const primaryChildInstance: OffscreenInstance = {
752755
_visibility: OffscreenVisible,
756+
_pendingVisibility: OffscreenVisible,
753757
_pendingMarkers: null,
754758
_retryCache: null,
755759
_transitions: null,
756760
_current: null,
757761
detach: () => detachOffscreenInstance(primaryChildInstance),
762+
attach: () => attachOffscreenInstance(primaryChildInstance),
758763
};
759764
fiber.stateNode = primaryChildInstance;
760765
return fiber;
@@ -773,11 +778,13 @@ export function createFiberFromLegacyHidden(
773778
// the offscreen implementation, which depends on a state node
774779
const instance: OffscreenInstance = {
775780
_visibility: OffscreenVisible,
781+
_pendingVisibility: OffscreenVisible,
776782
_pendingMarkers: null,
777783
_transitions: null,
778784
_retryCache: null,
779785
_current: null,
780786
detach: () => detachOffscreenInstance(instance),
787+
attach: () => attachOffscreenInstance(instance),
781788
};
782789
fiber.stateNode = instance;
783790
return fiber;

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,8 @@ function updateOffscreenComponent(
677677
) {
678678
const nextProps: OffscreenProps = workInProgress.pendingProps;
679679
const nextChildren = nextProps.children;
680+
const nextIsDetached =
681+
(workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0;
680682

681683
const prevState: OffscreenState | null =
682684
current !== null ? current.memoizedState : null;
@@ -687,8 +689,7 @@ function updateOffscreenComponent(
687689
nextProps.mode === 'hidden' ||
688690
(enableLegacyHidden &&
689691
nextProps.mode === 'unstable-defer-without-hiding') ||
690-
// TODO: remove read from stateNode.
691-
workInProgress.stateNode._visibility & OffscreenDetached
692+
nextIsDetached
692693
) {
693694
// Rendering a hidden tree.
694695

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
} from './ReactFiberHostConfig';
1818
import type {Fiber, FiberRoot} from './ReactInternalTypes';
1919
import type {Lanes} from './ReactFiberLane';
20+
import {NoTimestamp, SyncLane} from './ReactFiberLane';
2021
import type {SuspenseState} from './ReactFiberSuspenseComponent';
2122
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
2223
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
@@ -152,7 +153,6 @@ import {
152153
clearSingleton,
153154
acquireSingletonInstance,
154155
releaseSingletonInstance,
155-
scheduleMicrotask,
156156
} from './ReactFiberHostConfig';
157157
import {
158158
captureCommitPhaseError,
@@ -169,7 +169,6 @@ import {
169169
setIsRunningInsertionEffect,
170170
getExecutionContext,
171171
CommitContext,
172-
RenderContext,
173172
NoContext,
174173
} from './ReactFiberWorkLoop';
175174
import {
@@ -205,6 +204,8 @@ import {
205204
TransitionRoot,
206205
TransitionTracingMarker,
207206
} from './ReactFiberTracingMarkerComponent';
207+
import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
208+
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
208209

209210
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
210211
if (__DEV__) {
@@ -2407,24 +2408,44 @@ function getRetryCache(finishedWork) {
24072408
}
24082409

24092410
export function detachOffscreenInstance(instance: OffscreenInstance): void {
2410-
const currentOffscreenFiber = instance._current;
2411-
if (currentOffscreenFiber === null) {
2411+
const fiber = instance._current;
2412+
if (fiber === null) {
24122413
throw new Error(
24132414
'Calling Offscreen.detach before instance handle has been set.',
24142415
);
24152416
}
24162417

2417-
const executionContext = getExecutionContext();
2418-
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
2419-
scheduleMicrotask(() => {
2420-
instance._visibility |= OffscreenDetached;
2421-
disappearLayoutEffects(currentOffscreenFiber);
2422-
disconnectPassiveEffect(currentOffscreenFiber);
2423-
});
2424-
} else {
2425-
instance._visibility |= OffscreenDetached;
2426-
disappearLayoutEffects(currentOffscreenFiber);
2427-
disconnectPassiveEffect(currentOffscreenFiber);
2418+
if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) {
2419+
// The instance is already detached, this is a noop.
2420+
return;
2421+
}
2422+
2423+
// TODO: There is an opportunity to optimise this by not entering commit phase
2424+
// and unmounting effects directly.
2425+
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
2426+
if (root !== null) {
2427+
instance._pendingVisibility |= OffscreenDetached;
2428+
scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp);
2429+
}
2430+
}
2431+
2432+
export function attachOffscreenInstance(instance: OffscreenInstance): void {
2433+
const fiber = instance._current;
2434+
if (fiber === null) {
2435+
throw new Error(
2436+
'Calling Offscreen.detach before instance handle has been set.',
2437+
);
2438+
}
2439+
2440+
if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) {
2441+
// The instance is already attached, this is a noop.
2442+
return;
2443+
}
2444+
2445+
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
2446+
if (root !== null) {
2447+
instance._pendingVisibility &= ~OffscreenDetached;
2448+
scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp);
24282449
}
24292450
}
24302451

@@ -2857,12 +2878,19 @@ function commitMutationEffectsOnFiber(
28572878
}
28582879

28592880
commitReconciliationEffects(finishedWork);
2881+
2882+
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
2883+
28602884
// TODO: Add explicit effect flag to set _current.
2861-
finishedWork.stateNode._current = finishedWork;
2885+
offscreenInstance._current = finishedWork;
28622886

2863-
if (flags & Visibility) {
2864-
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
2887+
// Offscreen stores pending changes to visibility in `_pendingVisibility`. This is
2888+
// to support batching of `attach` and `detach` calls.
2889+
offscreenInstance._visibility &= ~OffscreenDetached;
2890+
offscreenInstance._visibility |=
2891+
offscreenInstance._pendingVisibility & OffscreenDetached;
28652892

2893+
if (flags & Visibility) {
28662894
// Track the current state on the Offscreen instance so we can
28672895
// read it during an event
28682896
if (isHidden) {

packages/react-reconciler/src/ReactFiberOffscreenComponent.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const OffscreenDetached = /* */ 0b010;
5050
export const OffscreenPassiveEffectsConnected = /* */ 0b100;
5151

5252
export type OffscreenInstance = {
53+
_pendingVisibility: OffscreenVisibility,
5354
_visibility: OffscreenVisibility,
5455
_pendingMarkers: Set<TracingMarkerInstance> | null,
5556
_transitions: Set<Transition> | null,
@@ -59,8 +60,7 @@ export type OffscreenInstance = {
5960
// Represents the current Offscreen fiber
6061
_current: Fiber | null,
6162
detach: () => void,
62-
63-
// TODO: attach
63+
attach: () => void,
6464
};
6565

6666
export function isOffscreenManual(offscreenFiber: Fiber): boolean {

0 commit comments

Comments
 (0)