[refactor] Add element type for Activity (#32499)
This PR separates Activity to it's own element type separate from Offscreen. The goal is to allow us to add Activity element boundary semantics during hydration similar to Suspense semantics, without impacting the Offscreen behavior in suspended children.
Ricky committed
Mar 17, 2025 at 09:17 UTC
1a191701fe5000098d23328b2ea9d70457fea1f8
18 files changed
+504
-95
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+166
@@ -135,6 +135,172 @@ describe('Store component filters', () => {
135
});
136
137
// @reactVersion >= 16.0
138
+ it('should filter Suspense', async () => {
139
+ const Suspense = React.Suspense;
140
+ await actAsync(async () =>
141
+ render(
142
+ <React.Fragment>
143
+ <Suspense>
144
+ <div>Visible</div>
145
+ </Suspense>
146
+ <Suspense>
147
+ <div>Hidden</div>
148
+ </Suspense>
149
+ </React.Fragment>,
150
+ ),
151
+ );
152
+
153
+ expect(store).toMatchInlineSnapshot(`
154
+ [root]
155
+ ▾ <Suspense>
156
+ <div>
157
+ ▾ <Suspense>
158
+ <div>
159
+ `);
160
+
161
+ await actAsync(
162
+ async () =>
163
+ (store.componentFilters = [
164
+ utils.createElementTypeFilter(Types.ElementTypeActivity),
165
+ ]),
166
+ );
167
+
168
+ expect(store).toMatchInlineSnapshot(`
169
+ [root]
170
+ ▾ <Suspense>
171
+ <div>
172
+ ▾ <Suspense>
173
+ <div>
174
+ `);
175
+
176
+ await actAsync(
177
+ async () =>
178
+ (store.componentFilters = [
179
+ utils.createElementTypeFilter(Types.ElementTypeActivity, false),
180
+ ]),
181
+ );
182
+
183
+ expect(store).toMatchInlineSnapshot(`
184
+ [root]
185
+ ▾ <Suspense>
186
+ <div>
187
+ ▾ <Suspense>
188
+ <div>
189
+ `);
190
+ });
191
+
192
+ it('should filter Activity', async () => {
193
+ const Activity = React.unstable_Activity;
194
+
195
+ if (Activity != null) {
196
+ await actAsync(async () =>
197
+ render(
198
+ <React.Fragment>
199
+ <Activity mode="visible">
200
+ <div>Visible</div>
201
+ </Activity>
202
+ <Activity mode="hidden">
203
+ <div>Hidden</div>
204
+ </Activity>
205
+ </React.Fragment>,
206
+ ),
207
+ );
208
+
209
+ expect(store).toMatchInlineSnapshot(`
210
+ [root]
211
+ ▾ <Activity>
212
+ <div>
213
+ ▾ <Activity>
214
+ <div>
215
+ `);
216
+
217
+ await actAsync(
218
+ async () =>
219
+ (store.componentFilters = [
220
+ utils.createElementTypeFilter(Types.ElementTypeActivity),
221
+ ]),
222
+ );
223
+
224
+ expect(store).toMatchInlineSnapshot(`
225
+ [root]
226
+ <div>
227
+ <div>
228
+ `);
229
+
230
+ await actAsync(
231
+ async () =>
232
+ (store.componentFilters = [
233
+ utils.createElementTypeFilter(Types.ElementTypeActivity, false),
234
+ ]),
235
+ );
236
+
237
+ expect(store).toMatchInlineSnapshot(`
238
+ [root]
239
+ ▾ <Activity>
240
+ <div>
241
+ ▾ <Activity>
242
+ <div>
243
+ `);
244
+ }
245
+ });
246
+
247
+ it('should filter ViewTransition', async () => {
248
+ const ViewTransition = React.unstable_ViewTransition;
249
+
250
+ if (ViewTransition != null) {
251
+ await actAsync(async () =>
252
+ render(
253
+ <React.Fragment>
254
+ <ViewTransition>
255
+ <div>Visible</div>
256
+ </ViewTransition>
257
+ <ViewTransition>
258
+ <div>Hidden</div>
259
+ </ViewTransition>
260
+ </React.Fragment>,
261
+ ),
262
+ );
263
+
264
+ expect(store).toMatchInlineSnapshot(`
265
+ [root]
266
+ ▾ <ViewTransition>
267
+ <div>
268
+ ▾ <ViewTransition>
269
+ <div>
270
+ `);
271
+
272
+ await actAsync(
273
+ async () =>
274
+ (store.componentFilters = [
275
+ utils.createElementTypeFilter(Types.ElementTypeActivity),
276
+ ]),
277
+ );
278
+
279
+ expect(store).toMatchInlineSnapshot(`
280
+ [root]
281
+ ▾ <ViewTransition>
282
+ <div>
283
+ ▾ <ViewTransition>
284
+ <div>
285
+ `);
286
+
287
+ await actAsync(
288
+ async () =>
289
+ (store.componentFilters = [
290
+ utils.createElementTypeFilter(Types.ElementTypeActivity, false),
291
+ ]),
292
+ );
293
+
294
+ expect(store).toMatchInlineSnapshot(`
295
+ [root]
296
+ ▾ <ViewTransition>
297
+ <div>
298
+ ▾ <ViewTransition>
299
+ <div>
300
+ `);
301
+ }
302
+ });
303
+
304
it('should ignore invalid ElementTypeRoot filter', async () => {
305
const Component = () => <div>Hi</div>;
306
packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js
+7
@@ -44,6 +44,7 @@ export function describeFiber(
44
ForwardRef,
45
ClassComponent,
46
ViewTransitionComponent,
47
+ ActivityComponent,
48
} = workTagMap;
49
50
switch (workInProgress.tag) {
@@ -60,6 +61,8 @@ export function describeFiber(
61
return describeBuiltInComponentFrame('SuspenseList');
62
case ViewTransitionComponent:
63
return describeBuiltInComponentFrame('ViewTransition');
64
+ case ActivityComponent:
65
+ return describeBuiltInComponentFrame('Activity');
66
case FunctionComponent:
67
case IndeterminateComponent:
68
case SimpleMemoComponent:
@@ -154,6 +157,7 @@ export function getOwnerStackByFiberInDev(
157
SuspenseComponent,
158
SuspenseListComponent,
159
ViewTransitionComponent,
160
+ ActivityComponent,
161
} = workTagMap;
162
try {
163
let info = '';
@@ -184,6 +188,9 @@ export function getOwnerStackByFiberInDev(
188
case ViewTransitionComponent:
189
info += describeBuiltInComponentFrame('ViewTransition');
190
break;
191
+ case ActivityComponent:
192
+ info += describeBuiltInComponentFrame('Activity');
193
+ break;
194
}
195
196
let owner: void | null | Fiber | ReactComponentInfo = workInProgress;
packages/react-devtools-shared/src/backend/fiber/renderer.js
+12
@@ -28,6 +28,7 @@ import {
28
ElementTypeSuspenseList,
29
ElementTypeTracingMarker,
30
ElementTypeViewTransition,
31
+ ElementTypeActivity,
32
ElementTypeVirtual,
33
StrictMode,
34
} from 'react-devtools-shared/src/frontend/types';
@@ -385,6 +386,7 @@ export function getInternalReactConstants(version: string): {
386
YieldComponent: -1, // Removed
387
Throw: 29,
388
ViewTransitionComponent: 30, // Experimental
389
+ ActivityComponent: 31,
390
};
391
} else if (gte(version, '17.0.0-alpha')) {
392
ReactTypeOfWork = {
@@ -421,6 +423,7 @@ export function getInternalReactConstants(version: string): {
423
YieldComponent: -1, // Removed
424
Throw: -1, // Doesn't exist yet
425
ViewTransitionComponent: -1, // Doesn't exist yet
426
+ ActivityComponent: -1, // Doesn't exist yet
427
};
428
} else if (gte(version, '16.6.0-beta.0')) {
429
ReactTypeOfWork = {
@@ -457,6 +460,7 @@ export function getInternalReactConstants(version: string): {
460
YieldComponent: -1, // Removed
461
Throw: -1, // Doesn't exist yet
462
ViewTransitionComponent: -1, // Doesn't exist yet
463
+ ActivityComponent: -1, // Doesn't exist yet
464
};
465
} else if (gte(version, '16.4.3-alpha')) {
466
ReactTypeOfWork = {
@@ -493,6 +497,7 @@ export function getInternalReactConstants(version: string): {
497
YieldComponent: -1, // Removed
498
Throw: -1, // Doesn't exist yet
499
ViewTransitionComponent: -1, // Doesn't exist yet
500
+ ActivityComponent: -1, // Doesn't exist yet
501
};
502
} else {
503
ReactTypeOfWork = {
@@ -529,6 +534,7 @@ export function getInternalReactConstants(version: string): {
534
YieldComponent: 9,
535
Throw: -1, // Doesn't exist yet
536
ViewTransitionComponent: -1, // Doesn't exist yet
537
+ ActivityComponent: -1, // Doesn't exist yet
538
};
539
}
540
// **********************************************************
@@ -572,6 +578,7 @@ export function getInternalReactConstants(version: string): {
578
TracingMarkerComponent,
579
Throw,
580
ViewTransitionComponent,
581
+ ActivityComponent,
582
} = ReactTypeOfWork;
583
584
function resolveFiberType(type: any): $FlowFixMe {
@@ -622,6 +629,8 @@ export function getInternalReactConstants(version: string): {
629
}
630
631
switch (tag) {
632
+ case ActivityComponent:
633
+ return 'Activity';
634
case CacheComponent:
635
return 'Cache';
636
case ClassComponent:
@@ -892,6 +901,7 @@ export function attach(
901
StrictModeBits,
902
} = getInternalReactConstants(version);
903
const {
904
+ ActivityComponent,
905
CacheComponent,
906
ClassComponent,
907
ContextConsumer,
@@ -1565,6 +1575,8 @@ export function attach(
1575
const {type, tag} = fiber;
1576
1577
switch (tag) {
1578
+ case ActivityComponent:
1579
+ return ElementTypeActivity;
1580
case ClassComponent:
1581
case IncompleteClassComponent:
1582
return ElementTypeClass;
packages/react-devtools-shared/src/backend/types.js
+1
@@ -77,6 +77,7 @@ export type WorkTagMap = {
77
YieldComponent: WorkTag,
78
Throw: WorkTag,
79
ViewTransitionComponent: WorkTag,
80
+ ActivityComponent: WorkTag,
81
};
82
83
export type HostInstance = Object;
packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js
+6
@@ -472,6 +472,8 @@ export default function ComponentsSettings({
472
((parseInt(currentTarget.value, 10): any): ElementType),
473
)
474
}>
475
+ {/* TODO: currently only experimental, only list this if it's available */}
476
+ {/*<option value={ElementTypeActivity}>activity</option>*/}
477
<option value={ElementTypeClass}>class</option>
478
<option value={ElementTypeContext}>context</option>
479
<option value={ElementTypeFunction}>function</option>
@@ -485,6 +487,10 @@ export default function ComponentsSettings({
487
<option value={ElementTypeOtherOrUnknown}>other</option>
488
<option value={ElementTypeProfiler}>profiler</option>
489
<option value={ElementTypeSuspense}>suspense</option>
490
+ {/* TODO: currently only experimental, only list this if it's available */}
491
+ {/*<option value={ElementTypeViewTransition}>*/}
492
+ {/* view transition*/}
493
+ {/*</option>*/}
494
</select>
495
)}
496
{(componentFilter.type === ComponentFilterLocation ||
packages/react-devtools-shared/src/frontend/types.js
+3
-1
@@ -50,6 +50,7 @@ export const ElementTypeSuspenseList = 13;
50
export const ElementTypeTracingMarker = 14;
51
export const ElementTypeVirtual = 15;
52
export const ElementTypeViewTransition = 16;
53
+export const ElementTypeActivity = 17;
54
55
// Different types of elements displayed in the Elements tree.
56
// These types may be used to visually distinguish types,
@@ -68,7 +69,8 @@ export type ElementType =
69
| 13
70
| 14
71
| 15
71
- | 16;
72
+ | 16
73
+ | 17;
74
75
// WARNING
76
// The values below are referenced by ComponentFilters (which are saved via localStorage).
packages/react-reconciler/src/ReactFiber.js
+15
@@ -71,6 +71,7 @@ import {
71
TracingMarkerComponent,
72
Throw,
73
ViewTransitionComponent,
74
+ ActivityComponent,
75
} from './ReactWorkTags';
76
import {OffscreenVisible} from './ReactFiberActivityComponent';
77
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
@@ -107,6 +108,7 @@ import {
108
REACT_TRACING_MARKER_TYPE,
109
REACT_ELEMENT_TYPE,
110
REACT_VIEW_TRANSITION_TYPE,
111
+ REACT_ACTIVITY_TYPE,
112
} from 'shared/ReactSymbols';
113
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
114
import {
@@ -588,6 +590,8 @@ export function createFiberFromTypeAndProps(
590
}
591
} else {
592
getTag: switch (type) {
593
+ case REACT_ACTIVITY_TYPE:
594
+ return createFiberFromActivity(pendingProps, mode, lanes, key);
595
case REACT_FRAGMENT_TYPE:
596
return createFiberFromFragment(pendingProps.children, mode, lanes, key);
597
case REACT_STRICT_MODE_TYPE:
@@ -865,6 +869,17 @@ export function createFiberFromOffscreen(
869
fiber.stateNode = primaryChildInstance;
870
return fiber;
871
}
872
+export function createFiberFromActivity(
873
+ pendingProps: OffscreenProps,
874
+ mode: TypeOfMode,
875
+ lanes: Lanes,
876
+ key: null | string,
877
+): Fiber {
878
+ const fiber = createFiber(ActivityComponent, pendingProps, key, mode);
879
+ fiber.elementType = REACT_ACTIVITY_TYPE;
880
+ fiber.lanes = lanes;
881
+ return fiber;
882
+}
883
884
export function createFiberFromViewTransition(
885
pendingProps: ViewTransitionProps,
packages/react-reconciler/src/ReactFiberBeginWork.js
+44
@@ -77,6 +77,7 @@ import {
77
TracingMarkerComponent,
78
Throw,
79
ViewTransitionComponent,
80
+ ActivityComponent,
81
} from './ReactWorkTags';
82
import {
83
NoFlags,
@@ -867,6 +868,46 @@ function deferHiddenOffscreenComponent(
868
// fork the function.
869
const updateLegacyHiddenComponent = updateOffscreenComponent;
870
871
+function updateActivityComponent(
872
+ current: null | Fiber,
873
+ workInProgress: Fiber,
874
+ renderLanes: Lanes,
875
+) {
876
+ const nextProps = workInProgress.pendingProps;
877
+ const nextChildren = nextProps.children;
878
+ const nextMode = nextProps.mode;
879
+ const mode = workInProgress.mode;
880
+ const offscreenChildProps: OffscreenProps = {
881
+ mode: nextMode,
882
+ children: nextChildren,
883
+ };
884
+
885
+ if (current === null) {
886
+ const primaryChildFragment = mountWorkInProgressOffscreenFiber(
887
+ offscreenChildProps,
888
+ mode,
889
+ renderLanes,
890
+ );
891
+ primaryChildFragment.ref = workInProgress.ref;
892
+ workInProgress.child = primaryChildFragment;
893
+ primaryChildFragment.return = workInProgress;
894
+
895
+ return primaryChildFragment;
896
+ } else {
897
+ const currentChild: Fiber = (current.child: any);
898
+
899
+ const primaryChildFragment = updateWorkInProgressOffscreenFiber(
900
+ currentChild,
901
+ offscreenChildProps,
902
+ );
903
+
904
+ primaryChildFragment.ref = workInProgress.ref;
905
+ workInProgress.child = primaryChildFragment;
906
+ primaryChildFragment.return = workInProgress;
907
+ return primaryChildFragment;
908
+ }
909
+}
910
+
911
function updateCacheComponent(
912
current: Fiber | null,
913
workInProgress: Fiber,
@@ -4025,6 +4066,9 @@ function beginWork(
4066
}
4067
break;
4068
}
4069
+ case ActivityComponent: {
4070
+ return updateActivityComponent(current, workInProgress, renderLanes);
4071
+ }
4072
case OffscreenComponent: {
4073
return updateOffscreenComponent(current, workInProgress, renderLanes);
4074
}
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2
@@ -76,6 +76,7 @@ import {
76
TracingMarkerComponent,
77
Throw,
78
ViewTransitionComponent,
79
+ ActivityComponent,
80
} from './ReactWorkTags';
81
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
82
import {
@@ -972,6 +973,7 @@ function completeWork(
973
}
974
// Fallthrough
975
}
976
+ case ActivityComponent:
977
case LazyComponent:
978
case SimpleMemoComponent:
979
case FunctionComponent:
packages/react-reconciler/src/ReactFiberComponentStack.js
+7
-1
@@ -24,6 +24,7 @@ import {
24
ClassComponent,
25
HostText,
26
ViewTransitionComponent,
27
+ ActivityComponent,
28
} from './ReactWorkTags';
29
import {
30
describeBuiltInComponentFrame,
@@ -53,6 +54,8 @@ function describeFiber(fiber: Fiber): string {
54
return describeFunctionComponentFrame(fiber.type.render);
55
case ClassComponent:
56
return describeClassComponentFrame(fiber.type);
57
+ case ActivityComponent:
58
+ return describeBuiltInComponentFrame('Activity');
59
case ViewTransitionComponent:
60
if (enableViewTransition) {
61
return describeBuiltInComponentFrame('ViewTransition');
@@ -129,9 +132,12 @@ export function getOwnerStackByFiberInDev(workInProgress: Fiber): string {
132
case SuspenseListComponent:
133
info += describeBuiltInComponentFrame('SuspenseList');
134
break;
135
+ case ActivityComponent:
136
+ info += describeBuiltInComponentFrame('Activity');
137
+ break;
138
case ViewTransitionComponent:
139
if (enableViewTransition) {
134
- info += describeBuiltInComponentFrame('SuspenseList');
140
+ info += describeBuiltInComponentFrame('ViewTransition');
141
break;
142
}
143
// Fallthrough
packages/react-reconciler/src/ReactWorkTags.js
+3
-1
@@ -38,7 +38,8 @@ export type WorkTag =
38
| 27
39
| 28
40
| 29
41
- | 30;
41
+ | 30
42
+ | 31;
43
44
export const FunctionComponent = 0;
45
export const ClassComponent = 1;
@@ -69,3 +70,4 @@ export const HostSingleton = 27;
70
export const IncompleteFunctionComponent = 28;
71
export const Throw = 29;
72
export const ViewTransitionComponent = 30;
73
+export const ActivityComponent = 31;
packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
+225
-87
@@ -10,17 +10,72 @@
10
'use strict';
11
12
let React;
13
+let Suspense;
14
+let Activity;
15
+let ViewTransition;
16
let ReactNoop;
17
let waitForAll;
18
19
describe('ReactFragment', () => {
20
+ let didCatchErrors = [];
21
+ let rootCaughtErrors = [];
22
+ let SomethingThatErrors;
23
+ let CatchingBoundary;
24
+ let onCaughtError;
25
+
26
beforeEach(function () {
27
jest.resetModules();
28
29
React = require('react');
30
+ Suspense = React.Suspense;
31
+ Activity = React.unstable_Activity;
32
+ ViewTransition = React.unstable_ViewTransition;
33
ReactNoop = require('react-noop-renderer');
34
const InternalTestUtils = require('internal-test-utils');
35
waitForAll = InternalTestUtils.waitForAll;
36
+
37
+ didCatchErrors = [];
38
+ rootCaughtErrors = [];
39
+
40
+ onCaughtError = function (error, errorInfo) {
41
+ rootCaughtErrors.push(
42
+ error.message,
43
+ normalizeCodeLocInfo(errorInfo.componentStack),
44
+ React.captureOwnerStack
45
+ ? normalizeCodeLocInfo(React.captureOwnerStack())
46
+ : null,
47
+ );
48
+ };
49
+
50
+ SomethingThatErrors = () => {
51
+ throw new Error('uh oh');
52
+ };
53
+
54
+ // eslint-disable-next-line no-shadow
55
+ CatchingBoundary = class CatchingBoundary extends React.Component {
56
+ constructor() {
57
+ super();
58
+ this.state = {};
59
+ }
60
+
61
+ static getDerivedStateFromError(error) {
62
+ return {errored: true};
63
+ }
64
+
65
+ componentDidCatch(err, errInfo) {
66
+ didCatchErrors.push(
67
+ err.message,
68
+ normalizeCodeLocInfo(errInfo.componentStack),
69
+ );
70
+ }
71
+
72
+ render() {
73
+ if (this.state.errored) {
74
+ return null;
75
+ }
76
+ return this.props.children;
77
+ }
78
+ };
79
});
80
81
function componentStack(components) {
@@ -38,21 +93,7 @@ describe('ReactFragment', () => {
93
);
94
}
95
41
- it('retains component stacks when rethrowing an error', async () => {
42
- function Foo() {
43
- return (
44
- <RethrowingBoundary>
45
- <Bar />
46
- </RethrowingBoundary>
47
- );
48
- }
49
- function Bar() {
50
- return <SomethingThatErrors />;
51
- }
52
- function SomethingThatErrors() {
53
- throw new Error('uh oh');
54
- }
55
-
96
+ it('retains component and owner stacks when rethrowing an error', async () => {
97
class RethrowingBoundary extends React.Component {
98
static getDerivedStateFromError(error) {
99
throw error;
@@ -63,33 +104,36 @@ describe('ReactFragment', () => {
104
}
105
}
106
66
- const errors = [];
67
- class CatchingBoundary extends React.Component {
68
- constructor() {
69
- super();
70
- this.state = {};
71
- }
72
- static getDerivedStateFromError(error) {
73
- return {errored: true};
74
- }
75
- componentDidCatch(err, errInfo) {
76
- errors.push(err.message, normalizeCodeLocInfo(errInfo.componentStack));
77
- }
78
- render() {
79
- if (this.state.errored) {
80
- return null;
81
- }
82
- return this.props.children;
83
- }
107
+ function Foo() {
108
+ return (
109
+ <RethrowingBoundary>
110
+ <Bar />
111
+ </RethrowingBoundary>
112
+ );
113
+ }
114
+ function Bar() {
115
+ return <SomethingThatErrors />;
116
}
117
86
- ReactNoop.render(
118
+ ReactNoop.createRoot({
119
+ onCaughtError,
120
+ }).render(
121
<CatchingBoundary>
122
<Foo />
123
</CatchingBoundary>,
124
);
125
await waitForAll([]);
92
- expect(errors).toEqual([
126
+ expect(didCatchErrors).toEqual([
127
+ 'uh oh',
128
+ componentStack([
129
+ 'SomethingThatErrors',
130
+ 'Bar',
131
+ 'RethrowingBoundary',
132
+ 'Foo',
133
+ 'CatchingBoundary',
134
+ ]),
135
+ ]);
136
+ expect(rootCaughtErrors).toEqual([
137
'uh oh',
138
componentStack([
139
'SomethingThatErrors',
@@ -98,77 +142,171 @@ describe('ReactFragment', () => {
142
'Foo',
143
'CatchingBoundary',
144
]),
145
+ __DEV__ ? componentStack(['Bar', 'Foo']) : null,
146
]);
147
});
148
104
- it('retains owner stacks when rethrowing an error', async () => {
105
- function Foo() {
106
- return (
107
- <RethrowingBoundary>
108
- <Bar />
109
- </RethrowingBoundary>
110
- );
111
- }
112
- function Bar() {
113
- return <SomethingThatErrors />;
114
- }
115
- function SomethingThatErrors() {
149
+ it('includes built-in for Suspense', async () => {
150
+ ReactNoop.createRoot({
151
+ onCaughtError,
152
+ }).render(
153
+ <CatchingBoundary>
154
+ <Suspense>
155
+ <SomethingThatErrors />
156
+ </Suspense>
157
+ </CatchingBoundary>,
158
+ );
159
+ await waitForAll([]);
160
+ expect(didCatchErrors).toEqual([
161
+ 'uh oh',
162
+ componentStack(['SomethingThatErrors', 'Suspense', 'CatchingBoundary']),
163
+ ]);
164
+ expect(rootCaughtErrors).toEqual([
165
+ 'uh oh',
166
+ componentStack(['SomethingThatErrors', 'Suspense', 'CatchingBoundary']),
167
+ __DEV__ ? componentStack(['SomethingThatErrors']) : null,
168
+ ]);
169
+ });
170
+
171
+ // @gate enableActivity
172
+ it('includes built-in for Activity', async () => {
173
+ ReactNoop.createRoot({
174
+ onCaughtError,
175
+ }).render(
176
+ <CatchingBoundary>
177
+ <Activity>
178
+ <SomethingThatErrors />
179
+ </Activity>
180
+ </CatchingBoundary>,
181
+ );
182
+ await waitForAll([]);
183
+ expect(didCatchErrors).toEqual([
184
+ 'uh oh',
185
+ componentStack(['SomethingThatErrors', 'Activity', 'CatchingBoundary']),
186
+ ]);
187
+ expect(rootCaughtErrors).toEqual([
188
+ 'uh oh',
189
+ componentStack(['SomethingThatErrors', 'Activity', 'CatchingBoundary']),
190
+ __DEV__ ? componentStack(['SomethingThatErrors']) : null,
191
+ ]);
192
+ });
193
+
194
+ // @gate enableViewTransition
195
+ it('includes built-in for ViewTransition', async () => {
196
+ ReactNoop.createRoot({
197
+ onCaughtError,
198
+ }).render(
199
+ <CatchingBoundary>
200
+ <ViewTransition>
201
+ <SomethingThatErrors />
202
+ </ViewTransition>
203
+ </CatchingBoundary>,
204
+ );
205
+ await waitForAll([]);
206
+ expect(didCatchErrors).toEqual([
207
+ 'uh oh',
208
+ componentStack([
209
+ 'SomethingThatErrors',
210
+ 'ViewTransition',
211
+ 'CatchingBoundary',
212
+ ]),
213
+ ]);
214
+ expect(rootCaughtErrors).toEqual([
215
+ 'uh oh',
216
+ componentStack([
217
+ 'SomethingThatErrors',
218
+ 'ViewTransition',
219
+ 'CatchingBoundary',
220
+ ]),
221
+ __DEV__ ? componentStack(['SomethingThatErrors']) : null,
222
+ ]);
223
+ });
224
+
225
+ it('includes built-in for Lazy', async () => {
226
+ // Lazy component throws
227
+ const LazyComponent = React.lazy(() => {
228
throw new Error('uh oh');
117
- }
229
+ });
230
119
- class RethrowingBoundary extends React.Component {
120
- static getDerivedStateFromError(error) {
121
- throw error;
122
- }
231
+ ReactNoop.createRoot({
232
+ onCaughtError,
233
+ }).render(
234
+ <CatchingBoundary>
235
+ <LazyComponent />
236
+ </CatchingBoundary>,
237
+ );
238
+ await waitForAll([]);
239
+ expect(didCatchErrors).toEqual([
240
+ 'uh oh',
241
+ componentStack(['Lazy', 'CatchingBoundary']),
242
+ ]);
243
+ expect(rootCaughtErrors).toEqual([
244
+ 'uh oh',
245
+ componentStack(['Lazy', 'CatchingBoundary']),
246
+ __DEV__ ? '' : null, // No owner stack
247
+ ]);
248
+ });
249
124
- render() {
125
- return this.props.children;
126
- }
127
- }
250
+ // @gate enableSuspenseList
251
+ it('includes built-in for SuspenseList', async () => {
252
+ const SuspenseList = React.unstable_SuspenseList;
253
129
- const errors = [];
130
- class CatchingBoundary extends React.Component {
131
- constructor() {
132
- super();
133
- this.state = {};
134
- }
135
- static getDerivedStateFromError(error) {
136
- return {errored: true};
137
- }
138
- render() {
139
- if (this.state.errored) {
140
- return null;
141
- }
142
- return this.props.children;
143
- }
144
- }
254
+ ReactNoop.createRoot({
255
+ onCaughtError,
256
+ }).render(
257
+ <CatchingBoundary>
258
+ <SuspenseList>
259
+ <SomethingThatErrors />
260
+ </SuspenseList>
261
+ </CatchingBoundary>,
262
+ );
263
+ await waitForAll([]);
264
+ expect(didCatchErrors).toEqual([
265
+ 'uh oh',
266
+ componentStack([
267
+ 'SomethingThatErrors',
268
+ 'SuspenseList',
269
+ 'CatchingBoundary',
270
+ ]),
271
+ ]);
272
+ expect(rootCaughtErrors).toEqual([
273
+ 'uh oh',
274
+ componentStack([
275
+ 'SomethingThatErrors',
276
+ 'SuspenseList',
277
+ 'CatchingBoundary',
278
+ ]),
279
+ __DEV__ ? componentStack(['SomethingThatErrors']) : null,
280
+ ]);
281
+ });
282
283
+ it('does not include built-in for Fragment', async () => {
284
ReactNoop.createRoot({
147
- onCaughtError(error, errorInfo) {
148
- errors.push(
149
- error.message,
150
- normalizeCodeLocInfo(errorInfo.componentStack),
151
- React.captureOwnerStack
152
- ? normalizeCodeLocInfo(React.captureOwnerStack())
153
- : null,
154
- );
155
- },
285
+ onCaughtError,
286
}).render(
287
<CatchingBoundary>
158
- <Foo />
288
+ <>
289
+ <SomethingThatErrors />
290
+ </>
291
</CatchingBoundary>,
292
);
293
await waitForAll([]);
162
- expect(errors).toEqual([
294
+ expect(didCatchErrors).toEqual([
295
'uh oh',
296
componentStack([
297
'SomethingThatErrors',
166
- 'Bar',
167
- 'RethrowingBoundary',
168
- 'Foo',
298
+ // No Fragment
299
'CatchingBoundary',
300
]),
171
- __DEV__ ? componentStack(['Bar', 'Foo']) : null,
301
+ ]);
302
+ expect(rootCaughtErrors).toEqual([
303
+ 'uh oh',
304
+ componentStack([
305
+ 'SomethingThatErrors',
306
+ // No Fragment
307
+ 'CatchingBoundary',
308
+ ]),
309
+ __DEV__ ? componentStack(['SomethingThatErrors']) : null,
310
]);
311
});
312
});
packages/react-reconciler/src/getComponentNameFromFiber.js
+3
@@ -47,6 +47,7 @@ import {
47
TracingMarkerComponent,
48
Throw,
49
ViewTransitionComponent,
50
+ ActivityComponent,
51
} from 'react-reconciler/src/ReactWorkTags';
52
import getComponentNameFromType from 'shared/getComponentNameFromType';
53
import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
@@ -85,6 +86,8 @@ export function getComponentNameFromOwner(
86
export default function getComponentNameFromFiber(fiber: Fiber): string | null {
87
const {tag, type} = fiber;
88
switch (tag) {
89
+ case ActivityComponent:
90
+ return 'Activity';
91
case CacheComponent:
92
return 'Cache';
93
case ContextConsumer:
packages/react-server/src/ReactFizzServer.js
+2
-2
@@ -151,9 +151,9 @@ import {
151
REACT_CONTEXT_TYPE,
152
REACT_CONSUMER_TYPE,
153
REACT_SCOPE_TYPE,
154
- REACT_OFFSCREEN_TYPE,
154
REACT_POSTPONE_TYPE,
155
REACT_VIEW_TRANSITION_TYPE,
156
+ REACT_ACTIVITY_TYPE,
157
} from 'shared/ReactSymbols';
158
import ReactSharedInternals from 'shared/ReactSharedInternals';
159
import {
@@ -2253,7 +2253,7 @@ function renderElement(
2253
task.keyPath = prevKeyPath;
2254
return;
2255
}
2256
- case REACT_OFFSCREEN_TYPE: {
2256
+ case REACT_ACTIVITY_TYPE: {
2257
renderOffscreen(request, task, keyPath, props);
2258
return;
2259
}
packages/react/src/ReactClient.js
+2
-2
@@ -15,7 +15,7 @@ import {
15
REACT_SUSPENSE_TYPE,
16
REACT_SUSPENSE_LIST_TYPE,
17
REACT_LEGACY_HIDDEN_TYPE,
18
- REACT_OFFSCREEN_TYPE,
18
+ REACT_ACTIVITY_TYPE,
19
REACT_SCOPE_TYPE,
20
REACT_TRACING_MARKER_TYPE,
21
REACT_VIEW_TRANSITION_TYPE,
@@ -116,7 +116,7 @@ export {
116
useDeferredValue,
117
REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
118
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
119
- REACT_OFFSCREEN_TYPE as unstable_Activity,
119
+ REACT_ACTIVITY_TYPE as unstable_Activity,
120
getCacheForType as unstable_getCacheForType,
121
useCacheRefresh as unstable_useCacheRefresh,
122
use,
packages/shared/ReactSymbols.js
+1
@@ -34,6 +34,7 @@ export const REACT_MEMO_TYPE: symbol = Symbol.for('react.memo');
34
export const REACT_LAZY_TYPE: symbol = Symbol.for('react.lazy');
35
export const REACT_SCOPE_TYPE: symbol = Symbol.for('react.scope');
36
export const REACT_OFFSCREEN_TYPE: symbol = Symbol.for('react.offscreen');
37
+export const REACT_ACTIVITY_TYPE: symbol = Symbol.for('react.activity');
38
export const REACT_LEGACY_HIDDEN_TYPE: symbol = Symbol.for(
39
'react.legacy_hidden',
40
);
packages/shared/getComponentNameFromType.js
+3
-1
@@ -25,6 +25,7 @@ import {
25
REACT_LAZY_TYPE,
26
REACT_TRACING_MARKER_TYPE,
27
REACT_VIEW_TRANSITION_TYPE,
28
+ REACT_ACTIVITY_TYPE,
29
} from 'shared/ReactSymbols';
30
31
import {
@@ -83,7 +84,8 @@ export default function getComponentNameFromType(type: mixed): string | null {
84
return 'Suspense';
85
case REACT_SUSPENSE_LIST_TYPE:
86
return 'SuspenseList';
86
- // Fall through
87
+ case REACT_ACTIVITY_TYPE:
88
+ return 'Activity';
89
case REACT_VIEW_TRANSITION_TYPE:
90
if (enableViewTransition) {
91
return 'ViewTransition';
packages/shared/isValidElementType.js
+2
@@ -24,6 +24,7 @@ import {
24
REACT_OFFSCREEN_TYPE,
25
REACT_TRACING_MARKER_TYPE,
26
REACT_VIEW_TRANSITION_TYPE,
27
+ REACT_ACTIVITY_TYPE,
28
} from 'shared/ReactSymbols';
29
import {
30
enableScopeAPI,
@@ -50,6 +51,7 @@ export default function isValidElementType(type: mixed): boolean {
51
type === REACT_SUSPENSE_TYPE ||
52
type === REACT_SUSPENSE_LIST_TYPE ||
53
(enableLegacyHidden && type === REACT_LEGACY_HIDDEN_TYPE) ||
54
+ type === REACT_ACTIVITY_TYPE ||
55
type === REACT_OFFSCREEN_TYPE ||
56
(enableScopeAPI && type === REACT_SCOPE_TYPE) ||
57
(enableTransitionTracing && type === REACT_TRACING_MARKER_TYPE) ||