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
};