@samitouri / QOS-React / commits / 1a191701fe

[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) ||