[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.
This commit is contained in:
Ricky 2025-03-17 09:17:00 -04:00 committed by GitHub
parent 99563e9173
commit 1a191701fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 513 additions and 104 deletions

View File

@ -135,6 +135,172 @@ describe('Store component filters', () => {
});
// @reactVersion >= 16.0
it('should filter Suspense', async () => {
const Suspense = React.Suspense;
await actAsync(async () =>
render(
<React.Fragment>
<Suspense>
<div>Visible</div>
</Suspense>
<Suspense>
<div>Hidden</div>
</Suspense>
</React.Fragment>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>
<div>
<Suspense>
<div>
`);
await actAsync(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeActivity),
]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>
<div>
<Suspense>
<div>
`);
await actAsync(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeActivity, false),
]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>
<div>
<Suspense>
<div>
`);
});
it('should filter Activity', async () => {
const Activity = React.unstable_Activity;
if (Activity != null) {
await actAsync(async () =>
render(
<React.Fragment>
<Activity mode="visible">
<div>Visible</div>
</Activity>
<Activity mode="hidden">
<div>Hidden</div>
</Activity>
</React.Fragment>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Activity>
<div>
<Activity>
<div>
`);
await actAsync(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeActivity),
]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<div>
<div>
`);
await actAsync(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeActivity, false),
]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Activity>
<div>
<Activity>
<div>
`);
}
});
it('should filter ViewTransition', async () => {
const ViewTransition = React.unstable_ViewTransition;
if (ViewTransition != null) {
await actAsync(async () =>
render(
<React.Fragment>
<ViewTransition>
<div>Visible</div>
</ViewTransition>
<ViewTransition>
<div>Hidden</div>
</ViewTransition>
</React.Fragment>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
<ViewTransition>
<div>
<ViewTransition>
<div>
`);
await actAsync(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeActivity),
]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<ViewTransition>
<div>
<ViewTransition>
<div>
`);
await actAsync(
async () =>
(store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeActivity, false),
]),
);
expect(store).toMatchInlineSnapshot(`
[root]
<ViewTransition>
<div>
<ViewTransition>
<div>
`);
}
});
it('should ignore invalid ElementTypeRoot filter', async () => {
const Component = () => <div>Hi</div>;

View File

@ -44,6 +44,7 @@ export function describeFiber(
ForwardRef,
ClassComponent,
ViewTransitionComponent,
ActivityComponent,
} = workTagMap;
switch (workInProgress.tag) {
@ -60,6 +61,8 @@ export function describeFiber(
return describeBuiltInComponentFrame('SuspenseList');
case ViewTransitionComponent:
return describeBuiltInComponentFrame('ViewTransition');
case ActivityComponent:
return describeBuiltInComponentFrame('Activity');
case FunctionComponent:
case IndeterminateComponent:
case SimpleMemoComponent:
@ -154,6 +157,7 @@ export function getOwnerStackByFiberInDev(
SuspenseComponent,
SuspenseListComponent,
ViewTransitionComponent,
ActivityComponent,
} = workTagMap;
try {
let info = '';
@ -184,6 +188,9 @@ export function getOwnerStackByFiberInDev(
case ViewTransitionComponent:
info += describeBuiltInComponentFrame('ViewTransition');
break;
case ActivityComponent:
info += describeBuiltInComponentFrame('Activity');
break;
}
let owner: void | null | Fiber | ReactComponentInfo = workInProgress;

View File

@ -28,6 +28,7 @@ import {
ElementTypeSuspenseList,
ElementTypeTracingMarker,
ElementTypeViewTransition,
ElementTypeActivity,
ElementTypeVirtual,
StrictMode,
} from 'react-devtools-shared/src/frontend/types';
@ -385,6 +386,7 @@ export function getInternalReactConstants(version: string): {
YieldComponent: -1, // Removed
Throw: 29,
ViewTransitionComponent: 30, // Experimental
ActivityComponent: 31,
};
} else if (gte(version, '17.0.0-alpha')) {
ReactTypeOfWork = {
@ -421,6 +423,7 @@ export function getInternalReactConstants(version: string): {
YieldComponent: -1, // Removed
Throw: -1, // Doesn't exist yet
ViewTransitionComponent: -1, // Doesn't exist yet
ActivityComponent: -1, // Doesn't exist yet
};
} else if (gte(version, '16.6.0-beta.0')) {
ReactTypeOfWork = {
@ -457,6 +460,7 @@ export function getInternalReactConstants(version: string): {
YieldComponent: -1, // Removed
Throw: -1, // Doesn't exist yet
ViewTransitionComponent: -1, // Doesn't exist yet
ActivityComponent: -1, // Doesn't exist yet
};
} else if (gte(version, '16.4.3-alpha')) {
ReactTypeOfWork = {
@ -493,6 +497,7 @@ export function getInternalReactConstants(version: string): {
YieldComponent: -1, // Removed
Throw: -1, // Doesn't exist yet
ViewTransitionComponent: -1, // Doesn't exist yet
ActivityComponent: -1, // Doesn't exist yet
};
} else {
ReactTypeOfWork = {
@ -529,6 +534,7 @@ export function getInternalReactConstants(version: string): {
YieldComponent: 9,
Throw: -1, // Doesn't exist yet
ViewTransitionComponent: -1, // Doesn't exist yet
ActivityComponent: -1, // Doesn't exist yet
};
}
// **********************************************************
@ -572,6 +578,7 @@ export function getInternalReactConstants(version: string): {
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
ActivityComponent,
} = ReactTypeOfWork;
function resolveFiberType(type: any): $FlowFixMe {
@ -622,6 +629,8 @@ export function getInternalReactConstants(version: string): {
}
switch (tag) {
case ActivityComponent:
return 'Activity';
case CacheComponent:
return 'Cache';
case ClassComponent:
@ -892,6 +901,7 @@ export function attach(
StrictModeBits,
} = getInternalReactConstants(version);
const {
ActivityComponent,
CacheComponent,
ClassComponent,
ContextConsumer,
@ -1565,6 +1575,8 @@ export function attach(
const {type, tag} = fiber;
switch (tag) {
case ActivityComponent:
return ElementTypeActivity;
case ClassComponent:
case IncompleteClassComponent:
return ElementTypeClass;

View File

@ -77,6 +77,7 @@ export type WorkTagMap = {
YieldComponent: WorkTag,
Throw: WorkTag,
ViewTransitionComponent: WorkTag,
ActivityComponent: WorkTag,
};
export type HostInstance = Object;

View File

@ -472,6 +472,8 @@ export default function ComponentsSettings({
((parseInt(currentTarget.value, 10): any): ElementType),
)
}>
{/* TODO: currently only experimental, only list this if it's available */}
{/*<option value={ElementTypeActivity}>activity</option>*/}
<option value={ElementTypeClass}>class</option>
<option value={ElementTypeContext}>context</option>
<option value={ElementTypeFunction}>function</option>
@ -485,6 +487,10 @@ export default function ComponentsSettings({
<option value={ElementTypeOtherOrUnknown}>other</option>
<option value={ElementTypeProfiler}>profiler</option>
<option value={ElementTypeSuspense}>suspense</option>
{/* TODO: currently only experimental, only list this if it's available */}
{/*<option value={ElementTypeViewTransition}>*/}
{/* view transition*/}
{/*</option>*/}
</select>
)}
{(componentFilter.type === ComponentFilterLocation ||

View File

@ -50,6 +50,7 @@ export const ElementTypeSuspenseList = 13;
export const ElementTypeTracingMarker = 14;
export const ElementTypeVirtual = 15;
export const ElementTypeViewTransition = 16;
export const ElementTypeActivity = 17;
// Different types of elements displayed in the Elements tree.
// These types may be used to visually distinguish types,
@ -68,7 +69,8 @@ export type ElementType =
| 13
| 14
| 15
| 16;
| 16
| 17;
// WARNING
// The values below are referenced by ComponentFilters (which are saved via localStorage).

View File

@ -71,6 +71,7 @@ import {
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
import {OffscreenVisible} from './ReactFiberActivityComponent';
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
@ -107,6 +108,7 @@ import {
REACT_TRACING_MARKER_TYPE,
REACT_ELEMENT_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
} from 'shared/ReactSymbols';
import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
import {
@ -588,6 +590,8 @@ export function createFiberFromTypeAndProps(
}
} else {
getTag: switch (type) {
case REACT_ACTIVITY_TYPE:
return createFiberFromActivity(pendingProps, mode, lanes, key);
case REACT_FRAGMENT_TYPE:
return createFiberFromFragment(pendingProps.children, mode, lanes, key);
case REACT_STRICT_MODE_TYPE:
@ -865,6 +869,17 @@ export function createFiberFromOffscreen(
fiber.stateNode = primaryChildInstance;
return fiber;
}
export function createFiberFromActivity(
pendingProps: OffscreenProps,
mode: TypeOfMode,
lanes: Lanes,
key: null | string,
): Fiber {
const fiber = createFiber(ActivityComponent, pendingProps, key, mode);
fiber.elementType = REACT_ACTIVITY_TYPE;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromViewTransition(
pendingProps: ViewTransitionProps,

View File

@ -77,6 +77,7 @@ import {
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
import {
NoFlags,
@ -867,6 +868,46 @@ function deferHiddenOffscreenComponent(
// fork the function.
const updateLegacyHiddenComponent = updateOffscreenComponent;
function updateActivityComponent(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
const nextMode = nextProps.mode;
const mode = workInProgress.mode;
const offscreenChildProps: OffscreenProps = {
mode: nextMode,
children: nextChildren,
};
if (current === null) {
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
offscreenChildProps,
mode,
renderLanes,
);
primaryChildFragment.ref = workInProgress.ref;
workInProgress.child = primaryChildFragment;
primaryChildFragment.return = workInProgress;
return primaryChildFragment;
} else {
const currentChild: Fiber = (current.child: any);
const primaryChildFragment = updateWorkInProgressOffscreenFiber(
currentChild,
offscreenChildProps,
);
primaryChildFragment.ref = workInProgress.ref;
workInProgress.child = primaryChildFragment;
primaryChildFragment.return = workInProgress;
return primaryChildFragment;
}
}
function updateCacheComponent(
current: Fiber | null,
workInProgress: Fiber,
@ -4025,6 +4066,9 @@ function beginWork(
}
break;
}
case ActivityComponent: {
return updateActivityComponent(current, workInProgress, renderLanes);
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}

View File

@ -76,6 +76,7 @@ import {
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {
@ -972,6 +973,7 @@ function completeWork(
}
// Fallthrough
}
case ActivityComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:

View File

@ -24,6 +24,7 @@ import {
ClassComponent,
HostText,
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
import {
describeBuiltInComponentFrame,
@ -53,6 +54,8 @@ function describeFiber(fiber: Fiber): string {
return describeFunctionComponentFrame(fiber.type.render);
case ClassComponent:
return describeClassComponentFrame(fiber.type);
case ActivityComponent:
return describeBuiltInComponentFrame('Activity');
case ViewTransitionComponent:
if (enableViewTransition) {
return describeBuiltInComponentFrame('ViewTransition');
@ -129,9 +132,12 @@ export function getOwnerStackByFiberInDev(workInProgress: Fiber): string {
case SuspenseListComponent:
info += describeBuiltInComponentFrame('SuspenseList');
break;
case ActivityComponent:
info += describeBuiltInComponentFrame('Activity');
break;
case ViewTransitionComponent:
if (enableViewTransition) {
info += describeBuiltInComponentFrame('SuspenseList');
info += describeBuiltInComponentFrame('ViewTransition');
break;
}
// Fallthrough

View File

@ -38,7 +38,8 @@ export type WorkTag =
| 27
| 28
| 29
| 30;
| 30
| 31;
export const FunctionComponent = 0;
export const ClassComponent = 1;
@ -69,3 +70,4 @@ export const HostSingleton = 27;
export const IncompleteFunctionComponent = 28;
export const Throw = 29;
export const ViewTransitionComponent = 30;
export const ActivityComponent = 31;

View File

@ -10,17 +10,72 @@
'use strict';
let React;
let Suspense;
let Activity;
let ViewTransition;
let ReactNoop;
let waitForAll;
describe('ReactFragment', () => {
let didCatchErrors = [];
let rootCaughtErrors = [];
let SomethingThatErrors;
let CatchingBoundary;
let onCaughtError;
beforeEach(function () {
jest.resetModules();
React = require('react');
Suspense = React.Suspense;
Activity = React.unstable_Activity;
ViewTransition = React.unstable_ViewTransition;
ReactNoop = require('react-noop-renderer');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
didCatchErrors = [];
rootCaughtErrors = [];
onCaughtError = function (error, errorInfo) {
rootCaughtErrors.push(
error.message,
normalizeCodeLocInfo(errorInfo.componentStack),
React.captureOwnerStack
? normalizeCodeLocInfo(React.captureOwnerStack())
: null,
);
};
SomethingThatErrors = () => {
throw new Error('uh oh');
};
// eslint-disable-next-line no-shadow
CatchingBoundary = class CatchingBoundary extends React.Component {
constructor() {
super();
this.state = {};
}
static getDerivedStateFromError(error) {
return {errored: true};
}
componentDidCatch(err, errInfo) {
didCatchErrors.push(
err.message,
normalizeCodeLocInfo(errInfo.componentStack),
);
}
render() {
if (this.state.errored) {
return null;
}
return this.props.children;
}
};
});
function componentStack(components) {
@ -38,21 +93,7 @@ describe('ReactFragment', () => {
);
}
it('retains component stacks when rethrowing an error', async () => {
function Foo() {
return (
<RethrowingBoundary>
<Bar />
</RethrowingBoundary>
);
}
function Bar() {
return <SomethingThatErrors />;
}
function SomethingThatErrors() {
throw new Error('uh oh');
}
it('retains component and owner stacks when rethrowing an error', async () => {
class RethrowingBoundary extends React.Component {
static getDerivedStateFromError(error) {
throw error;
@ -63,33 +104,26 @@ describe('ReactFragment', () => {
}
}
const errors = [];
class CatchingBoundary extends React.Component {
constructor() {
super();
this.state = {};
}
static getDerivedStateFromError(error) {
return {errored: true};
}
componentDidCatch(err, errInfo) {
errors.push(err.message, normalizeCodeLocInfo(errInfo.componentStack));
}
render() {
if (this.state.errored) {
return null;
}
return this.props.children;
function Foo() {
return (
<RethrowingBoundary>
<Bar />
</RethrowingBoundary>
);
}
function Bar() {
return <SomethingThatErrors />;
}
ReactNoop.render(
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<Foo />
</CatchingBoundary>,
);
await waitForAll([]);
expect(errors).toEqual([
expect(didCatchErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
@ -99,67 +133,7 @@ describe('ReactFragment', () => {
'CatchingBoundary',
]),
]);
});
it('retains owner stacks when rethrowing an error', async () => {
function Foo() {
return (
<RethrowingBoundary>
<Bar />
</RethrowingBoundary>
);
}
function Bar() {
return <SomethingThatErrors />;
}
function SomethingThatErrors() {
throw new Error('uh oh');
}
class RethrowingBoundary extends React.Component {
static getDerivedStateFromError(error) {
throw error;
}
render() {
return this.props.children;
}
}
const errors = [];
class CatchingBoundary extends React.Component {
constructor() {
super();
this.state = {};
}
static getDerivedStateFromError(error) {
return {errored: true};
}
render() {
if (this.state.errored) {
return null;
}
return this.props.children;
}
}
ReactNoop.createRoot({
onCaughtError(error, errorInfo) {
errors.push(
error.message,
normalizeCodeLocInfo(errorInfo.componentStack),
React.captureOwnerStack
? normalizeCodeLocInfo(React.captureOwnerStack())
: null,
);
},
}).render(
<CatchingBoundary>
<Foo />
</CatchingBoundary>,
);
await waitForAll([]);
expect(errors).toEqual([
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
@ -171,4 +145,168 @@ describe('ReactFragment', () => {
__DEV__ ? componentStack(['Bar', 'Foo']) : null,
]);
});
it('includes built-in for Suspense', async () => {
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<Suspense>
<SomethingThatErrors />
</Suspense>
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack(['SomethingThatErrors', 'Suspense', 'CatchingBoundary']),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack(['SomethingThatErrors', 'Suspense', 'CatchingBoundary']),
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
]);
});
// @gate enableActivity
it('includes built-in for Activity', async () => {
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<Activity>
<SomethingThatErrors />
</Activity>
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack(['SomethingThatErrors', 'Activity', 'CatchingBoundary']),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack(['SomethingThatErrors', 'Activity', 'CatchingBoundary']),
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
]);
});
// @gate enableViewTransition
it('includes built-in for ViewTransition', async () => {
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<ViewTransition>
<SomethingThatErrors />
</ViewTransition>
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'ViewTransition',
'CatchingBoundary',
]),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'ViewTransition',
'CatchingBoundary',
]),
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
]);
});
it('includes built-in for Lazy', async () => {
// Lazy component throws
const LazyComponent = React.lazy(() => {
throw new Error('uh oh');
});
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<LazyComponent />
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack(['Lazy', 'CatchingBoundary']),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack(['Lazy', 'CatchingBoundary']),
__DEV__ ? '' : null, // No owner stack
]);
});
// @gate enableSuspenseList
it('includes built-in for SuspenseList', async () => {
const SuspenseList = React.unstable_SuspenseList;
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<SuspenseList>
<SomethingThatErrors />
</SuspenseList>
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'SuspenseList',
'CatchingBoundary',
]),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'SuspenseList',
'CatchingBoundary',
]),
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
]);
});
it('does not include built-in for Fragment', async () => {
ReactNoop.createRoot({
onCaughtError,
}).render(
<CatchingBoundary>
<>
<SomethingThatErrors />
</>
</CatchingBoundary>,
);
await waitForAll([]);
expect(didCatchErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
// No Fragment
'CatchingBoundary',
]),
]);
expect(rootCaughtErrors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
// No Fragment
'CatchingBoundary',
]),
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
]);
});
});

View File

@ -47,6 +47,7 @@ import {
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
ActivityComponent,
} from 'react-reconciler/src/ReactWorkTags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
@ -85,6 +86,8 @@ export function getComponentNameFromOwner(
export default function getComponentNameFromFiber(fiber: Fiber): string | null {
const {tag, type} = fiber;
switch (tag) {
case ActivityComponent:
return 'Activity';
case CacheComponent:
return 'Cache';
case ContextConsumer:

View File

@ -151,9 +151,9 @@ import {
REACT_CONTEXT_TYPE,
REACT_CONSUMER_TYPE,
REACT_SCOPE_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_POSTPONE_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@ -2253,7 +2253,7 @@ function renderElement(
task.keyPath = prevKeyPath;
return;
}
case REACT_OFFSCREEN_TYPE: {
case REACT_ACTIVITY_TYPE: {
renderOffscreen(request, task, keyPath, props);
return;
}

View File

@ -15,7 +15,7 @@ import {
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_ACTIVITY_TYPE,
REACT_SCOPE_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
@ -116,7 +116,7 @@ export {
useDeferredValue,
REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList,
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
REACT_OFFSCREEN_TYPE as unstable_Activity,
REACT_ACTIVITY_TYPE as unstable_Activity,
getCacheForType as unstable_getCacheForType,
useCacheRefresh as unstable_useCacheRefresh,
use,

View File

@ -34,6 +34,7 @@ export const REACT_MEMO_TYPE: symbol = Symbol.for('react.memo');
export const REACT_LAZY_TYPE: symbol = Symbol.for('react.lazy');
export const REACT_SCOPE_TYPE: symbol = Symbol.for('react.scope');
export const REACT_OFFSCREEN_TYPE: symbol = Symbol.for('react.offscreen');
export const REACT_ACTIVITY_TYPE: symbol = Symbol.for('react.activity');
export const REACT_LEGACY_HIDDEN_TYPE: symbol = Symbol.for(
'react.legacy_hidden',
);

View File

@ -25,6 +25,7 @@ import {
REACT_LAZY_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
} from 'shared/ReactSymbols';
import {
@ -83,7 +84,8 @@ export default function getComponentNameFromType(type: mixed): string | null {
return 'Suspense';
case REACT_SUSPENSE_LIST_TYPE:
return 'SuspenseList';
// Fall through
case REACT_ACTIVITY_TYPE:
return 'Activity';
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return 'ViewTransition';

View File

@ -24,6 +24,7 @@ import {
REACT_OFFSCREEN_TYPE,
REACT_TRACING_MARKER_TYPE,
REACT_VIEW_TRANSITION_TYPE,
REACT_ACTIVITY_TYPE,
} from 'shared/ReactSymbols';
import {
enableScopeAPI,
@ -50,6 +51,7 @@ export default function isValidElementType(type: mixed): boolean {
type === REACT_SUSPENSE_TYPE ||
type === REACT_SUSPENSE_LIST_TYPE ||
(enableLegacyHidden && type === REACT_LEGACY_HIDDEN_TYPE) ||
type === REACT_ACTIVITY_TYPE ||
type === REACT_OFFSCREEN_TYPE ||
(enableScopeAPI && type === REACT_SCOPE_TYPE) ||
(enableTransitionTracing && type === REACT_TRACING_MARKER_TYPE) ||