@samitouri / QOS-React / commits / 31ecc9804a

Clarify that there's three different kinds of OffscreenProps (#32838)

ActivityProps - Public API LegacyHiddenProps - Public Legacy API OffscreenProps - Internal implementation detail

Sebastian Markbåge committed Apr 9, 2025 at 22:22 UTC 31ecc9804a3f263033611f069774e50059c0743a
14 files changed +134 -76
packages/react-reconciler/src/ReactFiber.js
+6 -4
@@ -15,10 +15,12 @@ import type {WorkTag} from './ReactWorkTags';
15 import type {TypeOfMode} from './ReactTypeOfMode';
16 import type {Lanes} from './ReactFiberLane';
17 import type {SuspenseInstance} from './ReactFiberConfig';
18 +import type {ActivityProps} from './ReactFiberActivityComponent';
19 import type {
20 + LegacyHiddenProps,
21 OffscreenProps,
22 OffscreenInstance,
21 -} from './ReactFiberActivityComponent';
23 +} from './ReactFiberOffscreenComponent';
24 import type {
25 ViewTransitionProps,
26 ViewTransitionState,
@@ -74,7 +76,7 @@ import {
76 ViewTransitionComponent,
77 ActivityComponent,
78 } from './ReactWorkTags';
77 -import {OffscreenVisible} from './ReactFiberActivityComponent';
79 +import {OffscreenVisible} from './ReactFiberOffscreenComponent';
80 import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
81 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
82 import {
@@ -860,7 +862,7 @@ export function createFiberFromOffscreen(
862 return fiber;
863 }
864 export function createFiberFromActivity(
863 - pendingProps: OffscreenProps,
865 + pendingProps: ActivityProps,
866 mode: TypeOfMode,
867 lanes: Lanes,
868 key: null | string,
@@ -896,7 +898,7 @@ export function createFiberFromViewTransition(
898 }
899
900 export function createFiberFromLegacyHidden(
899 - pendingProps: OffscreenProps,
901 + pendingProps: LegacyHiddenProps,
902 mode: TypeOfMode,
903 lanes: Lanes,
904 key: null | string,
packages/react-reconciler/src/ReactFiberActivityComponent.js
+3 -42
@@ -7,48 +7,9 @@
7 * @flow
8 */
9
10 -import type {ReactNodeList, OffscreenMode, Wakeable} from 'shared/ReactTypes';
11 -import type {Lanes} from './ReactFiberLane';
12 -import type {SpawnedCachePool} from './ReactFiberCacheComponent';
13 -import type {Transition} from 'react/src/ReactStartTransition';
14 -import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
15 -import type {RetryQueue} from './ReactFiberSuspenseComponent';
10 +import type {ReactNodeList} from 'shared/ReactTypes';
11
17 -export type OffscreenProps = {
18 - // TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
19 - // for now, since we might have multiple variants. For example, hiding the
20 - // content without changing the layout.
21 - //
22 - // Default mode is visible. Kind of a weird default for a component
23 - // called "Offscreen." Possible alt: <Visibility />?
24 - mode?: OffscreenMode | null | void,
12 +export type ActivityProps = {
13 + mode?: 'hidden' | 'visible' | null | void,
14 children?: ReactNodeList,
15 };
27 -
28 -// We use the existence of the state object as an indicator that the component
29 -// is hidden.
30 -export type OffscreenState = {
31 - // TODO: This doesn't do anything, yet. It's always NoLanes. But eventually it
32 - // will represent the pending work that must be included in the render in
33 - // order to unhide the component.
34 - baseLanes: Lanes,
35 - cachePool: SpawnedCachePool | null,
36 -};
37 -
38 -export type OffscreenQueue = {
39 - transitions: Array<Transition> | null,
40 - markerInstances: Array<TracingMarkerInstance> | null,
41 - retryQueue: RetryQueue | null,
42 -};
43 -
44 -type OffscreenVisibility = number;
45 -
46 -export const OffscreenVisible = /* */ 0b001;
47 -export const OffscreenPassiveEffectsConnected = /* */ 0b010;
48 -
49 -export type OffscreenInstance = {
50 - _visibility: OffscreenVisibility,
51 - _pendingMarkers: Set<TracingMarkerInstance> | null,
52 - _transitions: Set<Transition> | null,
53 - _retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
54 -};
packages/react-reconciler/src/ReactFiberApplyGesture.js
+1 -1
@@ -11,7 +11,7 @@ import type {Fiber, FiberRoot} from './ReactInternalTypes';
11
12 import type {Instance, TextInstance, Props} from './ReactFiberConfig';
13
14 -import type {OffscreenState} from './ReactFiberActivityComponent';
14 +import type {OffscreenState} from './ReactFiberOffscreenComponent';
15
16 import type {
17 ViewTransitionState,
packages/react-reconciler/src/ReactFiberBeginWork.js
+48 -11
@@ -22,12 +22,14 @@ import type {
22 SuspenseListTailMode,
23 } from './ReactFiberSuspenseComponent';
24 import type {SuspenseContext} from './ReactFiberSuspenseContext';
25 +import type {ActivityProps} from './ReactFiberActivityComponent';
26 import type {
27 + LegacyHiddenProps,
28 OffscreenProps,
29 OffscreenState,
30 OffscreenQueue,
31 OffscreenInstance,
30 -} from './ReactFiberActivityComponent';
32 +} from './ReactFiberOffscreenComponent';
33 import type {
34 ViewTransitionProps,
35 ViewTransitionState,
@@ -645,8 +647,8 @@ function updateOffscreenComponent(
647 current: Fiber | null,
648 workInProgress: Fiber,
649 renderLanes: Lanes,
650 + nextProps: OffscreenProps,
651 ) {
649 - const nextProps: OffscreenProps = workInProgress.pendingProps;
652 const nextChildren = nextProps.children;
653
654 const prevState: OffscreenState | null =
@@ -858,17 +860,30 @@ function deferHiddenOffscreenComponent(
860 return null;
861 }
862
861 -// Note: These happen to have identical begin phases, for now. We shouldn't hold
862 -// ourselves to this constraint, though. If the behavior diverges, we should
863 -// fork the function.
864 -const updateLegacyHiddenComponent = updateOffscreenComponent;
863 +function updateLegacyHiddenComponent(
864 + current: null | Fiber,
865 + workInProgress: Fiber,
866 + renderLanes: Lanes,
867 +) {
868 + const nextProps: LegacyHiddenProps = workInProgress.pendingProps;
869 + // Note: These happen to have identical begin phases, for now. We shouldn't hold
870 + // ourselves to this constraint, though. If the behavior diverges, we should
871 + // fork the function.
872 + // This just works today because it has the same Props.
873 + return updateOffscreenComponent(
874 + current,
875 + workInProgress,
876 + renderLanes,
877 + nextProps,
878 + );
879 +}
880
881 function updateActivityComponent(
882 current: null | Fiber,
883 workInProgress: Fiber,
884 renderLanes: Lanes,
885 ) {
871 - const nextProps = workInProgress.pendingProps;
886 + const nextProps: ActivityProps = workInProgress.pendingProps;
887 const nextChildren = nextProps.children;
888 const nextMode = nextProps.mode;
889 const mode = workInProgress.mode;
@@ -3797,8 +3812,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3812 return null;
3813 }
3814 }
3800 - case OffscreenComponent:
3801 - case LegacyHiddenComponent: {
3815 + case OffscreenComponent: {
3816 // Need to check if the tree still needs to be deferred. This is
3817 // almost identical to the logic used in the normal update path,
3818 // so we'll just enter that. The only difference is we'll bail out
@@ -3808,7 +3822,12 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3822 // path from the normal path. I'm tempted to do a labeled break here
3823 // but I won't :)
3824 workInProgress.lanes = NoLanes;
3811 - return updateOffscreenComponent(current, workInProgress, renderLanes);
3825 + return updateOffscreenComponent(
3826 + current,
3827 + workInProgress,
3828 + renderLanes,
3829 + workInProgress.pendingProps,
3830 + );
3831 }
3832 case CacheComponent: {
3833 const cache: Cache = current.memoizedState.cache;
@@ -3821,7 +3840,20 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3840 if (instance !== null) {
3841 pushMarkerInstance(workInProgress, instance);
3842 }
3843 + break;
3844 + }
3845 + // Fallthrough
3846 + }
3847 + case LegacyHiddenComponent: {
3848 + if (enableLegacyHidden) {
3849 + workInProgress.lanes = NoLanes;
3850 + return updateLegacyHiddenComponent(
3851 + current,
3852 + workInProgress,
3853 + renderLanes,
3854 + );
3855 }
3856 + // Fallthrough
3857 }
3858 }
3859 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
@@ -4087,7 +4119,12 @@ function beginWork(
4119 return updateActivityComponent(current, workInProgress, renderLanes);
4120 }
4121 case OffscreenComponent: {
4090 - return updateOffscreenComponent(current, workInProgress, renderLanes);
4122 + return updateOffscreenComponent(
4123 + current,
4124 + workInProgress,
4125 + renderLanes,
4126 + workInProgress.pendingProps,
4127 + );
4128 }
4129 case LegacyHiddenComponent: {
4130 if (enableLegacyHidden) {
packages/react-reconciler/src/ReactFiberCommitWork.js
+2 -2
@@ -30,7 +30,7 @@ import type {
30 OffscreenState,
31 OffscreenInstance,
32 OffscreenQueue,
33 -} from './ReactFiberActivityComponent';
33 +} from './ReactFiberOffscreenComponent';
34 import type {Cache} from './ReactFiberCacheComponent';
35 import type {RootState} from './ReactFiberRoot';
36 import type {Transition} from 'react/src/ReactStartTransition';
@@ -201,7 +201,7 @@ import {clearTransitionsForLanes} from './ReactFiberLane';
201 import {
202 OffscreenVisible,
203 OffscreenPassiveEffectsConnected,
204 -} from './ReactFiberActivityComponent';
204 +} from './ReactFiberOffscreenComponent';
205 import {
206 TransitionRoot,
207 TransitionTracingMarker,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+1 -1
@@ -27,7 +27,7 @@ import type {
27 import type {
28 OffscreenState,
29 OffscreenQueue,
30 -} from './ReactFiberActivityComponent';
30 +} from './ReactFiberOffscreenComponent';
31 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
32 import type {Cache} from './ReactFiberCacheComponent';
33 import {
packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
+2 -2
@@ -17,7 +17,7 @@ import type {
17 Update as ClassUpdate,
18 } from './ReactFiberClassUpdateQueue';
19 import type {Lane, Lanes} from './ReactFiberLane';
20 -import type {OffscreenInstance} from './ReactFiberActivityComponent';
20 +import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
21
22 import {
23 warnAboutUpdateOnNotYetMountedFiberInDEV,
@@ -27,7 +27,7 @@ import {
27 import {NoLane, NoLanes, mergeLanes, markHiddenUpdate} from './ReactFiberLane';
28 import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
29 import {HostRoot, OffscreenComponent} from './ReactWorkTags';
30 -import {OffscreenVisible} from './ReactFiberActivityComponent';
30 +import {OffscreenVisible} from './ReactFiberOffscreenComponent';
31
32 export type ConcurrentUpdate = {
33 next: ConcurrentUpdate,
packages/react-reconciler/src/ReactFiberOffscreenComponent.js new
+61
@@ -0,0 +1,61 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
11 +import type {Lanes} from './ReactFiberLane';
12 +import type {SpawnedCachePool} from './ReactFiberCacheComponent';
13 +import type {Transition} from 'react/src/ReactStartTransition';
14 +import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
15 +import type {RetryQueue} from './ReactFiberSuspenseComponent';
16 +
17 +type OffscreenMode = 'hidden' | 'unstable-defer-without-hiding' | 'visible';
18 +
19 +export type LegacyHiddenProps = {
20 + mode?: OffscreenMode | null | void,
21 + children?: ReactNodeList,
22 +};
23 +
24 +export type OffscreenProps = {
25 + // TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
26 + // for now, since we might have multiple variants. For example, hiding the
27 + // content without changing the layout.
28 + //
29 + // Default mode is visible. Kind of a weird default for a component
30 + // called "Offscreen." Possible alt: <Visibility />?
31 + mode?: OffscreenMode | null | void,
32 + children?: ReactNodeList,
33 +};
34 +
35 +// We use the existence of the state object as an indicator that the component
36 +// is hidden.
37 +export type OffscreenState = {
38 + // TODO: This doesn't do anything, yet. It's always NoLanes. But eventually it
39 + // will represent the pending work that must be included in the render in
40 + // order to unhide the component.
41 + baseLanes: Lanes,
42 + cachePool: SpawnedCachePool | null,
43 +};
44 +
45 +export type OffscreenQueue = {
46 + transitions: Array<Transition> | null,
47 + markerInstances: Array<TracingMarkerInstance> | null,
48 + retryQueue: RetryQueue | null,
49 +};
50 +
51 +type OffscreenVisibility = number;
52 +
53 +export const OffscreenVisible = /* */ 0b001;
54 +export const OffscreenPassiveEffectsConnected = /* */ 0b010;
55 +
56 +export type OffscreenInstance = {
57 + _visibility: OffscreenVisibility,
58 + _pendingMarkers: Set<TracingMarkerInstance> | null,
59 + _transitions: Set<Transition> | null,
60 + _retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
61 +};
packages/react-reconciler/src/ReactFiberSuspenseContext.js
+1 -1
@@ -10,7 +10,7 @@
10 import type {Fiber} from './ReactInternalTypes';
11 import type {StackCursor} from './ReactFiberStack';
12 import type {SuspenseProps, SuspenseState} from './ReactFiberSuspenseComponent';
13 -import type {OffscreenState} from './ReactFiberActivityComponent';
13 +import type {OffscreenState} from './ReactFiberOffscreenComponent';
14
15 import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
16 import {createCursor, push, pop} from './ReactFiberStack';
packages/react-reconciler/src/ReactFiberThrow.js
+1 -1
@@ -12,7 +12,7 @@ import type {Lane, Lanes} from './ReactFiberLane';
12 import type {CapturedValue} from './ReactCapturedValue';
13 import type {Update} from './ReactFiberClassUpdateQueue';
14 import type {Wakeable} from 'shared/ReactTypes';
15 -import type {OffscreenQueue} from './ReactFiberActivityComponent';
15 +import type {OffscreenQueue} from './ReactFiberOffscreenComponent';
16 import type {RetryQueue} from './ReactFiberSuspenseComponent';
17
18 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.js
+1 -1
@@ -13,7 +13,7 @@ import type {
13 FiberRoot,
14 } from './ReactInternalTypes';
15 import type {Transition} from 'react/src/ReactStartTransition';
16 -import type {OffscreenInstance} from './ReactFiberActivityComponent';
16 +import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
17 import type {StackCursor} from './ReactFiberStack';
18
19 import {enableTransitionTracing} from 'shared/ReactFeatureFlags';
packages/react-reconciler/src/ReactFiberWorkLoop.js
+1 -1
@@ -20,7 +20,7 @@ import type {
20 PendingBoundaries,
21 TransitionAbort,
22 } from './ReactFiberTracingMarkerComponent';
23 -import type {OffscreenInstance} from './ReactFiberActivityComponent';
23 +import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
24 import type {
25 Resource,
26 ViewTransitionInstance,
packages/react-server/src/ReactFizzServer.js
+6 -4
@@ -16,7 +16,6 @@ import type {
16 ReactNodeList,
17 ReactContext,
18 ReactConsumerType,
19 - OffscreenMode,
19 Wakeable,
20 Thenable,
21 ReactFormState,
@@ -35,6 +34,9 @@ import type {ContextSnapshot} from './ReactFizzNewContext';
34 import type {ComponentStackNode} from './ReactFizzComponentStack';
35 import type {TreeContext} from './ReactFizzTreeContext';
36 import type {ThenableState} from './ReactFizzThenable';
37 +
38 +import type {ActivityProps} from 'react-reconciler/src/ReactFiberActivityComponent';
39 +
40 import {describeObjectForErrorMessage} from 'shared/ReactSerializationErrors';
41
42 import {
@@ -2206,12 +2208,12 @@ function renderActivity(
2208 request: Request,
2209 task: Task,
2210 keyPath: KeyNode,
2209 - props: Object,
2211 + props: ActivityProps,
2212 ): void {
2213 const segment = task.blockedSegment;
2214 if (segment === null) {
2215 // Replay
2214 - const mode: ?OffscreenMode = (props.mode: any);
2216 + const mode = props.mode;
2217 if (mode === 'hidden') {
2218 // A hidden Activity boundary is not server rendered. Prerendering happens
2219 // on the client.
@@ -2227,7 +2229,7 @@ function renderActivity(
2229 // An Activity boundary is delimited so that we can hydrate it separately.
2230 pushStartActivityBoundary(segment.chunks, request.renderState);
2231 segment.lastPushedText = false;
2230 - const mode: ?OffscreenMode = (props.mode: any);
2232 + const mode = props.mode;
2233 if (mode === 'hidden') {
2234 // A hidden Activity boundary is not server rendered. Prerendering happens
2235 // on the client.
packages/shared/ReactTypes.js
-5
@@ -137,11 +137,6 @@ export type Thenable<T> =
137 | FulfilledThenable<T>
138 | RejectedThenable<T>;
139
140 -export type OffscreenMode =
141 - | 'hidden'
142 - | 'unstable-defer-without-hiding'
143 - | 'visible';
144 -
140 export type StartTransitionOptions = {
141 name?: string,
142 };