Skip to content

Commit 3b551c8

Browse files
authored
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

Lines changed: 17 additions & 3 deletions
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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 26 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 6 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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
});

0 commit comments

Comments
 (0)