Skip to content

Commit

Permalink
Offscreen add attach (#25603)
Browse files Browse the repository at this point in the history
`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 <git@andrewclark.io>
  • Loading branch information
sammy-SC and acdlite committed Dec 12, 2022
1 parent b14d7fa commit 996e4c0
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 43 deletions.
9 changes: 8 additions & 1 deletion packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ import {
REACT_TRACING_MARKER_TYPE,
} from 'shared/ReactSymbols';
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
import {detachOffscreenInstance} from './ReactFiberCommitWork';
import {
detachOffscreenInstance,
attachOffscreenInstance,
} from './ReactFiberCommitWork';
import {getHostContext} from './ReactFiberHostContext';

export type {Fiber};
Expand Down Expand Up @@ -750,11 +753,13 @@ export function createFiberFromOffscreen(
fiber.lanes = lanes;
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingVisibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
_current: null,
detach: () => detachOffscreenInstance(primaryChildInstance),
attach: () => attachOffscreenInstance(primaryChildInstance),
};
fiber.stateNode = primaryChildInstance;
return fiber;
Expand All @@ -773,11 +778,13 @@ export function createFiberFromLegacyHidden(
// the offscreen implementation, which depends on a state node
const instance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingVisibility: OffscreenVisible,
_pendingMarkers: null,
_transitions: null,
_retryCache: null,
_current: null,
detach: () => detachOffscreenInstance(instance),
attach: () => attachOffscreenInstance(instance),
};
fiber.stateNode = instance;
return fiber;
Expand Down
5 changes: 3 additions & 2 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@ function updateOffscreenComponent(
) {
const nextProps: OffscreenProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
const nextIsDetached =
(workInProgress.stateNode._pendingVisibility & OffscreenDetached) !== 0;

const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
Expand All @@ -687,8 +689,7 @@ function updateOffscreenComponent(
nextProps.mode === 'hidden' ||
(enableLegacyHidden &&
nextProps.mode === 'unstable-defer-without-hiding') ||
// TODO: remove read from stateNode.
workInProgress.stateNode._visibility & OffscreenDetached
nextIsDetached
) {
// Rendering a hidden tree.

Expand Down
64 changes: 46 additions & 18 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from './ReactFiberHostConfig';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
import {NoTimestamp, SyncLane} from './ReactFiberLane';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
Expand Down Expand Up @@ -152,7 +153,6 @@ import {
clearSingleton,
acquireSingletonInstance,
releaseSingletonInstance,
scheduleMicrotask,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
Expand All @@ -169,7 +169,6 @@ import {
setIsRunningInsertionEffect,
getExecutionContext,
CommitContext,
RenderContext,
NoContext,
} from './ReactFiberWorkLoop';
import {
Expand Down Expand Up @@ -205,6 +204,8 @@ import {
TransitionRoot,
TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent';
import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';

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

export function detachOffscreenInstance(instance: OffscreenInstance): void {
const currentOffscreenFiber = instance._current;
if (currentOffscreenFiber === null) {
const fiber = instance._current;
if (fiber === null) {
throw new Error(
'Calling Offscreen.detach before instance handle has been set.',
);
}

const executionContext = getExecutionContext();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
scheduleMicrotask(() => {
instance._visibility |= OffscreenDetached;
disappearLayoutEffects(currentOffscreenFiber);
disconnectPassiveEffect(currentOffscreenFiber);
});
} else {
instance._visibility |= OffscreenDetached;
disappearLayoutEffects(currentOffscreenFiber);
disconnectPassiveEffect(currentOffscreenFiber);
if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) {
// The instance is already detached, this is a noop.
return;
}

// TODO: There is an opportunity to optimise this by not entering commit phase
// and unmounting effects directly.
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
instance._pendingVisibility |= OffscreenDetached;
scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp);
}
}

export function attachOffscreenInstance(instance: OffscreenInstance): void {
const fiber = instance._current;
if (fiber === null) {
throw new Error(
'Calling Offscreen.detach before instance handle has been set.',
);
}

if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) {
// The instance is already attached, this is a noop.
return;
}

const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
instance._pendingVisibility &= ~OffscreenDetached;
scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp);
}
}

Expand Down Expand Up @@ -2857,12 +2878,19 @@ function commitMutationEffectsOnFiber(
}

commitReconciliationEffects(finishedWork);

const offscreenInstance: OffscreenInstance = finishedWork.stateNode;

// TODO: Add explicit effect flag to set _current.
finishedWork.stateNode._current = finishedWork;
offscreenInstance._current = finishedWork;

if (flags & Visibility) {
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
// Offscreen stores pending changes to visibility in `_pendingVisibility`. This is
// to support batching of `attach` and `detach` calls.
offscreenInstance._visibility &= ~OffscreenDetached;
offscreenInstance._visibility |=
offscreenInstance._pendingVisibility & OffscreenDetached;

if (flags & Visibility) {
// Track the current state on the Offscreen instance so we can
// read it during an event
if (isHidden) {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactFiberOffscreenComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const OffscreenDetached = /* */ 0b010;
export const OffscreenPassiveEffectsConnected = /* */ 0b100;

export type OffscreenInstance = {
_pendingVisibility: OffscreenVisibility,
_visibility: OffscreenVisibility,
_pendingMarkers: Set<TracingMarkerInstance> | null,
_transitions: Set<Transition> | null,
Expand All @@ -59,8 +60,7 @@ export type OffscreenInstance = {
// Represents the current Offscreen fiber
_current: Fiber | null,
detach: () => void,

// TODO: attach
attach: () => void,
};

export function isOffscreenManual(offscreenFiber: Fiber): boolean {
Expand Down
Loading

0 comments on commit 996e4c0

Please sign in to comment.