Skip to content

Commit 3b551c8

Browse files
authoredApr 22, 2024··
Rename the react.element symbol to react.transitional.element (#28813)
We have changed the shape (and the runtime) of React Elements. To help avoid precompiled or inlined JSX having subtle breakages or deopting hidden classes, I renamed the symbol so that we can early error if private implementation details are used or mismatching versions are used. Why "transitional"? Well, because this is not the last time we'll change the shape. This is just a stepping stone to removing the `ref` field on the elements in the next version so we'll likely have to do it again.
1 parent db913d8 commit 3b551c8

18 files changed

+345
-227
lines changed
 

‎packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js

+17-3
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,23 @@ describe('InspectedElementContext', () => {
290290
"preview_long": {boolean: true, number: 123, string: "abc"},
291291
},
292292
},
293-
"react_element": Dehydrated {
294-
"preview_short": <span />,
295-
"preview_long": <span />,
293+
"react_element": {
294+
"$$typeof": Dehydrated {
295+
"preview_short": Symbol(react.element),
296+
"preview_long": Symbol(react.element),
297+
},
298+
"_owner": null,
299+
"_store": Dehydrated {
300+
"preview_short": {…},
301+
"preview_long": {},
302+
},
303+
"key": null,
304+
"props": Dehydrated {
305+
"preview_short": {…},
306+
"preview_long": {},
307+
},
308+
"ref": null,
309+
"type": "span",
296310
},
297311
"regexp": Dehydrated {
298312
"preview_short": /abc/giu,

‎packages/react-devtools-shared/src/backend/ReactSymbols.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export const SERVER_CONTEXT_SYMBOL_STRING = 'Symbol(react.server_context)';
2323

2424
export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)';
2525

26-
export const ELEMENT_NUMBER = 0xeac7;
27-
export const ELEMENT_SYMBOL_STRING = 'Symbol(react.element)';
26+
export const ELEMENT_SYMBOL_STRING = 'Symbol(react.transitional.element)';
27+
export const LEGACY_ELEMENT_NUMBER = 0xeac7;
28+
export const LEGACY_ELEMENT_SYMBOL_STRING = 'Symbol(react.element)';
2829

2930
export const DEBUG_TRACING_MODE_NUMBER = 0xeae1;
3031
export const DEBUG_TRACING_MODE_SYMBOL_STRING =

‎packages/react-dom/src/__tests__/ReactComponent-test.js

+26
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,32 @@ describe('ReactComponent', () => {
612612
);
613613
});
614614

615+
// @gate renameElementSymbol
616+
it('throws if a legacy element is used as a child', async () => {
617+
const inlinedElement = {
618+
$$typeof: Symbol.for('react.element'),
619+
type: 'div',
620+
key: null,
621+
ref: null,
622+
props: {},
623+
_owner: null,
624+
};
625+
const element = <div>{[inlinedElement]}</div>;
626+
const container = document.createElement('div');
627+
const root = ReactDOMClient.createRoot(container);
628+
await expect(
629+
act(() => {
630+
root.render(element);
631+
}),
632+
).rejects.toThrowError(
633+
'A React Element from an older version of React was rendered. ' +
634+
'This is not supported. It can happen if:\n' +
635+
'- Multiple copies of the "react" package is used.\n' +
636+
'- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
637+
'- A compiler tries to "inline" JSX instead of using the runtime.',
638+
);
639+
});
640+
615641
it('throws if a plain object even if it is in an owner', async () => {
616642
class Foo extends React.Component {
617643
render() {

‎packages/react-dom/src/__tests__/ReactDOMOption-test.js

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ describe('ReactDOMOption', () => {
134134
}).rejects.toThrow('Objects are not valid as a React child');
135135
});
136136

137+
// @gate www
137138
it('should support element-ish child', async () => {
138139
// This is similar to <fbt>.
139140
// We don't toString it because you must instead provide a value prop.

‎packages/react-dom/src/__tests__/refs-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ describe('ref swapping', () => {
382382
}).rejects.toThrow('Expected ref to be a function');
383383
});
384384

385-
// @gate !enableRefAsProp
385+
// @gate !enableRefAsProp && www
386386
it('undefined ref on manually inlined React element triggers error', async () => {
387387
const container = document.createElement('div');
388388
const root = ReactDOMClient.createRoot(container);

‎packages/react-reconciler/src/ReactChildFiber.js

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
REACT_PORTAL_TYPE,
3333
REACT_LAZY_TYPE,
3434
REACT_CONTEXT_TYPE,
35+
REACT_LEGACY_ELEMENT_TYPE,
3536
} from 'shared/ReactSymbols';
3637
import {
3738
HostRoot,
@@ -166,6 +167,16 @@ function coerceRef(
166167
}
167168

168169
function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
170+
if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) {
171+
throw new Error(
172+
'A React Element from an older version of React was rendered. ' +
173+
'This is not supported. It can happen if:\n' +
174+
'- Multiple copies of the "react" package is used.\n' +
175+
'- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
176+
'- A compiler tries to "inline" JSX instead of using the runtime.',
177+
);
178+
}
179+
169180
// $FlowFixMe[method-unbinding]
170181
const childString = Object.prototype.toString.call(newChild);
171182

‎packages/react/src/jsx/ReactJSXElement.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ function elementRefGetterWithDeprecationWarning() {
162162
/**
163163
* Factory method to create a new React element. This no longer adheres to
164164
* the class pattern, so do not use new to call it. Also, instanceof check
165-
* will not work. Instead test $$typeof field against Symbol.for('react.element') to check
165+
* will not work. Instead test $$typeof field against Symbol.for('react.transitional.element') to check
166166
* if something is a React Element.
167167
*
168168
* @param {*} type

‎packages/shared/ReactFeatureFlags.js

+3
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ export const transitionLaneExpirationMs = 5000;
143143

144144
// const __NEXT_MAJOR__ = __EXPERIMENTAL__;
145145

146+
// Renames the internal symbol for elements since they have changed signature/constructor
147+
export const renameElementSymbol = true;
148+
146149
// Removes legacy style context
147150
export const disableLegacyContext = true;
148151

‎packages/shared/ReactSymbols.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
* @flow
88
*/
99

10+
import {renameElementSymbol} from 'shared/ReactFeatureFlags';
11+
1012
// ATTENTION
1113
// When adding new symbols to this file,
1214
// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'
1315

1416
// The Symbol used to tag the ReactElement-like types.
15-
export const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.element');
17+
export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element');
18+
export const REACT_ELEMENT_TYPE: symbol = renameElementSymbol
19+
? Symbol.for('react.transitional.element')
20+
: REACT_LEGACY_ELEMENT_TYPE;
1621
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
1722
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
1823
export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode');

‎packages/shared/__tests__/ReactSymbols-test.internal.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('ReactSymbols', () => {
2323
});
2424
};
2525

26+
// @gate renameElementSymbol
2627
it('Symbol values should be unique', () => {
2728
expectToBeUnique(Object.entries(require('shared/ReactSymbols')));
2829
});

‎packages/shared/forks/ReactFeatureFlags.native-fb.js

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export const enableLegacyFBSupport = false;
6969
export const enableFilterEmptyStringAttributesDOM = true;
7070
export const enableGetInspectorDataForInstanceInProduction = true;
7171

72+
export const renameElementSymbol = false;
73+
7274
export const enableRetryLaneExpiration = false;
7375
export const retryLaneExpirationMs = 5000;
7476
export const syncLaneExpirationMs = 250;

‎packages/shared/forks/ReactFeatureFlags.native-oss.js

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
104104
export const passChildrenWhenCloningPersistedNodes = false;
105105
export const enableEarlyReturnForPropDiffing = false;
106106

107+
export const renameElementSymbol = true;
108+
107109
// Profiling Only
108110
export const enableProfilerTimer = __PROFILE__;
109111
export const enableProfilerCommitHooks = __PROFILE__;

‎packages/shared/forks/ReactFeatureFlags.test-renderer.js

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export const enableServerComponentLogs = true;
7979
export const enableInfiniteRenderLoopDetection = false;
8080
export const enableEarlyReturnForPropDiffing = false;
8181

82+
export const renameElementSymbol = true;
83+
8284
// TODO: This must be in sync with the main ReactFeatureFlags file because
8385
// the Test Renderer's value must be the same as the one used by the
8486
// react package.

‎packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js

+2
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,7 @@ export const disableDOMTestUtils = false;
9090
export const disableDefaultPropsExceptForClasses = false;
9191
export const enableEarlyReturnForPropDiffing = false;
9292

93+
export const renameElementSymbol = false;
94+
9395
// Flow magic to verify the exports of this file match the original version.
9496
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

‎packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

+2
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,7 @@ export const disableDOMTestUtils = false;
9090
export const disableDefaultPropsExceptForClasses = false;
9191
export const enableEarlyReturnForPropDiffing = false;
9292

93+
export const renameElementSymbol = false;
94+
9395
// Flow magic to verify the exports of this file match the original version.
9496
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

‎packages/shared/forks/ReactFeatureFlags.www.js

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export const enableSchedulingProfiler: boolean =
6565
export const disableLegacyContext = __EXPERIMENTAL__;
6666
export const enableGetInspectorDataForInstanceInProduction = false;
6767

68+
export const renameElementSymbol = false;
69+
6870
export const enableCache = true;
6971
export const enableLegacyCache = true;
7072
export const enableFetchInstrumentation = false;

0 commit comments

Comments
 (0)
Please sign in to comment.