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.
Sebastian Markbåge committed
Apr 22, 2024 at 12:39 UTC
3b551c82844bcfde51f0febb8e42c1a0d777df2c
18 files changed
+345
-227
packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
+17
-3
@@ -290,9 +290,23 @@ describe('InspectedElementContext', () => {
290
"preview_long": {boolean: true, number: 123, string: "abc"},
291
},
292
},
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",
310
},
311
"regexp": Dehydrated {
312
"preview_short": /abc/giu,
packages/react-devtools-shared/src/backend/ReactSymbols.js
+3
-2
@@ -23,8 +23,9 @@ export const SERVER_CONTEXT_SYMBOL_STRING = 'Symbol(react.server_context)';
23
24
export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)';
25
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)';
29
30
export const DEBUG_TRACING_MODE_NUMBER = 0xeae1;
31
export const DEBUG_TRACING_MODE_SYMBOL_STRING =
packages/react-dom/src/__tests__/ReactComponent-test.js
+26
@@ -612,6 +612,32 @@ describe('ReactComponent', () => {
612
);
613
});
614
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
+
641
it('throws if a plain object even if it is in an owner', async () => {
642
class Foo extends React.Component {
643
render() {
packages/react-dom/src/__tests__/ReactDOMOption-test.js
+1
@@ -134,6 +134,7 @@ describe('ReactDOMOption', () => {
134
}).rejects.toThrow('Objects are not valid as a React child');
135
});
136
137
+ // @gate www
138
it('should support element-ish child', async () => {
139
// This is similar to <fbt>.
140
// We don't toString it because you must instead provide a value prop.
packages/react-dom/src/__tests__/refs-test.js
+1
-1
@@ -382,7 +382,7 @@ describe('ref swapping', () => {
382
}).rejects.toThrow('Expected ref to be a function');
383
});
384
385
- // @gate !enableRefAsProp
385
+ // @gate !enableRefAsProp && www
386
it('undefined ref on manually inlined React element triggers error', async () => {
387
const container = document.createElement('div');
388
const root = ReactDOMClient.createRoot(container);
packages/react-reconciler/src/ReactChildFiber.js
+11
@@ -32,6 +32,7 @@ import {
32
REACT_PORTAL_TYPE,
33
REACT_LAZY_TYPE,
34
REACT_CONTEXT_TYPE,
35
+ REACT_LEGACY_ELEMENT_TYPE,
36
} from 'shared/ReactSymbols';
37
import {
38
HostRoot,
@@ -166,6 +167,16 @@ function coerceRef(
167
}
168
169
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
+
180
// $FlowFixMe[method-unbinding]
181
const childString = Object.prototype.toString.call(newChild);
182
packages/react/src/jsx/ReactJSXElement.js
+1
-1
@@ -162,7 +162,7 @@ function elementRefGetterWithDeprecationWarning() {
162
/**
163
* Factory method to create a new React element. This no longer adheres to
164
* 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
166
* if something is a React Element.
167
*
168
* @param {*} type
packages/shared/ReactFeatureFlags.js
+3
@@ -143,6 +143,9 @@ export const transitionLaneExpirationMs = 5000;
143
144
// const __NEXT_MAJOR__ = __EXPERIMENTAL__;
145
146
+// Renames the internal symbol for elements since they have changed signature/constructor
147
+export const renameElementSymbol = true;
148
+
149
// Removes legacy style context
150
export const disableLegacyContext = true;
151
packages/shared/ReactSymbols.js
+6
-1
@@ -7,12 +7,17 @@
7
* @flow
8
*/
9
10
+import {renameElementSymbol} from 'shared/ReactFeatureFlags';
11
+
12
// ATTENTION
13
// When adding new symbols to this file,
14
// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'
15
16
// 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;
21
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
22
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
23
export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode');
packages/shared/__tests__/ReactSymbols-test.internal.js
+1
@@ -23,6 +23,7 @@ describe('ReactSymbols', () => {
23
});
24
};
25
26
+ // @gate renameElementSymbol
27
it('Symbol values should be unique', () => {
28
expectToBeUnique(Object.entries(require('shared/ReactSymbols')));
29
});
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2
@@ -69,6 +69,8 @@ export const enableLegacyFBSupport = false;
69
export const enableFilterEmptyStringAttributesDOM = true;
70
export const enableGetInspectorDataForInstanceInProduction = true;
71
72
+export const renameElementSymbol = false;
73
+
74
export const enableRetryLaneExpiration = false;
75
export const retryLaneExpirationMs = 5000;
76
export const syncLaneExpirationMs = 250;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -104,6 +104,8 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
104
export const passChildrenWhenCloningPersistedNodes = false;
105
export const enableEarlyReturnForPropDiffing = false;
106
107
+export const renameElementSymbol = true;
108
+
109
// Profiling Only
110
export const enableProfilerTimer = __PROFILE__;
111
export const enableProfilerCommitHooks = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -79,6 +79,8 @@ export const enableServerComponentLogs = true;
79
export const enableInfiniteRenderLoopDetection = false;
80
export const enableEarlyReturnForPropDiffing = false;
81
82
+export const renameElementSymbol = true;
83
+
84
// TODO: This must be in sync with the main ReactFeatureFlags file because
85
// the Test Renderer's value must be the same as the one used by the
86
// react package.
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+2
@@ -90,5 +90,7 @@ export const disableDOMTestUtils = false;
90
export const disableDefaultPropsExceptForClasses = false;
91
export const enableEarlyReturnForPropDiffing = false;
92
93
+export const renameElementSymbol = false;
94
+
95
// Flow magic to verify the exports of this file match the original version.
96
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -90,5 +90,7 @@ export const disableDOMTestUtils = false;
90
export const disableDefaultPropsExceptForClasses = false;
91
export const enableEarlyReturnForPropDiffing = false;
92
93
+export const renameElementSymbol = false;
94
+
95
// Flow magic to verify the exports of this file match the original version.
96
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -65,6 +65,8 @@ export const enableSchedulingProfiler: boolean =
65
export const disableLegacyContext = __EXPERIMENTAL__;
66
export const enableGetInspectorDataForInstanceInProduction = false;
67
68
+export const renameElementSymbol = false;
69
+
70
export const enableCache = true;
71
export const enableLegacyCache = true;
72
export const enableFetchInstrumentation = false;
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+261
-218
@@ -20,7 +20,6 @@ let useState;
20
let useEffect;
21
let useLayoutEffect;
22
let assertLog;
23
-
23
let originalError;
24
25
// This tests shared behavior between the built-in and shim implementations of
@@ -28,7 +27,6 @@ let originalError;
27
describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
28
beforeEach(() => {
29
jest.resetModules();
31
-
30
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
31
// Test the shim against React 17.
32
jest.mock('react', () => {
@@ -49,7 +47,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
47
originalError = console.error;
48
console.error = jest.fn();
49
}
52
-
50
React = require('react');
51
ReactDOM = require('react-dom');
52
ReactDOMClient = require('react-dom/client');
@@ -57,17 +54,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
54
useState = React.useState;
55
useEffect = React.useEffect;
56
useLayoutEffect = React.useLayoutEffect;
60
-
57
const InternalTestUtils = require('internal-test-utils');
58
assertLog = InternalTestUtils.assertLog;
63
-
59
const internalAct = require('internal-test-utils').act;
60
61
// The internal act implementation doesn't batch updates by default, since
62
// it's mostly used to test concurrent mode. But since these tests run
63
// in both concurrent and legacy mode, I'm adding batching here.
64
act = cb => internalAct(() => ReactDOM.unstable_batchedUpdates(cb));
70
-
65
if (gate(flags => flags.source)) {
66
// The `shim/with-selector` module composes the main
67
// `use-sync-external-store` entrypoint. In the compiled artifacts, this
@@ -84,18 +78,15 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
78
useSyncExternalStoreWithSelector =
79
require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
80
});
87
-
81
afterEach(() => {
82
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
83
console.error = originalError;
84
}
85
});
93
-
86
function Text({text}) {
87
Scheduler.log(text);
88
return text;
89
}
98
-
90
function createRoot(container) {
91
// This wrapper function exists so we can test both legacy roots and
92
// concurrent roots.
@@ -117,7 +108,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
108
};
109
}
110
}
120
-
111
function createExternalStore(initialState) {
112
const listeners = new Set();
113
let currentState = initialState;
@@ -140,41 +130,36 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
130
},
131
};
132
}
143
-
133
it('basic usage', async () => {
134
const store = createExternalStore('Initial');
146
-
135
function App() {
136
const text = useSyncExternalStore(store.subscribe, store.getState);
149
- return <Text text={text} />;
137
+ return React.createElement(Text, {
138
+ text: text,
139
+ });
140
}
151
-
141
const container = document.createElement('div');
142
const root = createRoot(container);
154
- await act(() => root.render(<App />));
155
-
143
+ await act(() => root.render(React.createElement(App, null)));
144
assertLog(['Initial']);
145
expect(container.textContent).toEqual('Initial');
158
-
146
await act(() => {
147
store.set('Updated');
148
});
149
assertLog(['Updated']);
150
expect(container.textContent).toEqual('Updated');
151
});
165
-
152
it('skips re-rendering if nothing changes', async () => {
153
const store = createExternalStore('Initial');
168
-
154
function App() {
155
const text = useSyncExternalStore(store.subscribe, store.getState);
171
- return <Text text={text} />;
156
+ return React.createElement(Text, {
157
+ text: text,
158
+ });
159
}
173
-
160
const container = document.createElement('div');
161
const root = createRoot(container);
176
- await act(() => root.render(<App />));
177
-
162
+ await act(() => root.render(React.createElement(App, null)));
163
assertLog(['Initial']);
164
expect(container.textContent).toEqual('Initial');
165
@@ -186,26 +171,23 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
171
assertLog([]);
172
expect(container.textContent).toEqual('Initial');
173
});
189
-
174
it('switch to a different store', async () => {
175
const storeA = createExternalStore(0);
176
const storeB = createExternalStore(0);
193
-
177
let setStore;
178
function App() {
179
const [store, _setStore] = useState(storeA);
180
setStore = _setStore;
181
const value = useSyncExternalStore(store.subscribe, store.getState);
199
- return <Text text={value} />;
182
+ return React.createElement(Text, {
183
+ text: value,
184
+ });
185
}
201
-
186
const container = document.createElement('div');
187
const root = createRoot(container);
204
- await act(() => root.render(<App />));
205
-
188
+ await act(() => root.render(React.createElement(App, null)));
189
assertLog([0]);
190
expect(container.textContent).toEqual('0');
208
-
191
await act(() => {
192
storeA.set(1);
193
});
@@ -239,38 +221,43 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
221
assertLog([1]);
222
expect(container.textContent).toEqual('1');
223
});
242
-
224
it('selecting a specific value inside getSnapshot', async () => {
244
- const store = createExternalStore({a: 0, b: 0});
245
-
225
+ const store = createExternalStore({
226
+ a: 0,
227
+ b: 0,
228
+ });
229
function A() {
230
const a = useSyncExternalStore(store.subscribe, () => store.getState().a);
248
- return <Text text={'A' + a} />;
231
+ return React.createElement(Text, {
232
+ text: 'A' + a,
233
+ });
234
}
235
function B() {
236
const b = useSyncExternalStore(store.subscribe, () => store.getState().b);
252
- return <Text text={'B' + b} />;
237
+ return React.createElement(Text, {
238
+ text: 'B' + b,
239
+ });
240
}
254
-
241
function App() {
256
- return (
257
- <>
258
- <A />
259
- <B />
260
- </>
242
+ return React.createElement(
243
+ React.Fragment,
244
+ null,
245
+ React.createElement(A, null),
246
+ React.createElement(B, null),
247
);
248
}
263
-
249
const container = document.createElement('div');
250
const root = createRoot(container);
266
- await act(() => root.render(<App />));
267
-
251
+ await act(() => root.render(React.createElement(App, null)));
252
assertLog(['A0', 'B0']);
253
expect(container.textContent).toEqual('A0B0');
254
255
// Update b but not a
256
await act(() => {
273
- store.set({a: 0, b: 1});
257
+ store.set({
258
+ a: 0,
259
+ b: 1,
260
+ });
261
});
262
// Only b re-renders
263
assertLog(['B1']);
@@ -278,7 +265,10 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
265
266
// Update a but not b
267
await act(() => {
281
- store.set({a: 1, b: 1});
268
+ store.set({
269
+ a: 1,
270
+ b: 1,
271
+ });
272
});
273
// Only a re-renders
274
assertLog(['A1']);
@@ -293,18 +283,18 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
283
'mutation in between the sync and passive effects',
284
async () => {
285
const store = createExternalStore(0);
296
-
286
function App() {
287
const value = useSyncExternalStore(store.subscribe, store.getState);
288
useEffect(() => {
289
Scheduler.log('Passive effect: ' + value);
290
}, [value]);
302
- return <Text text={value} />;
291
+ return React.createElement(Text, {
292
+ text: value,
293
+ });
294
}
304
-
295
const container = document.createElement('div');
296
const root = createRoot(container);
307
- await act(() => root.render(<App />));
297
+ await act(() => root.render(React.createElement(App, null)));
298
assertLog([0, 'Passive effect: 0']);
299
300
// Schedule an update. We'll intentionally not use `act` so that we can
@@ -331,13 +321,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
321
expect(container.textContent).toEqual('0');
322
},
323
);
334
-
324
it('mutating the store in between render and commit when getSnapshot has changed', async () => {
336
- const store = createExternalStore({a: 1, b: 1});
337
-
325
+ const store = createExternalStore({
326
+ a: 1,
327
+ b: 1,
328
+ });
329
const getSnapshotA = () => store.getState().a;
330
const getSnapshotB = () => store.getState().b;
340
-
331
function Child1({step}) {
332
const value = useSyncExternalStore(store.subscribe, store.getState);
333
useLayoutEffect(() => {
@@ -347,37 +337,42 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
337
// fired yet, so it doesn't have access to the latest getSnapshot. So
338
// it can't use the getSnapshot to bail out.
339
Scheduler.log('Update B in commit phase');
350
- store.set({a: value.a, b: 2});
340
+ store.set({
341
+ a: value.a,
342
+ b: 2,
343
+ });
344
}
345
}, [step]);
346
return null;
347
}
355
-
348
function Child2({step}) {
349
const label = step === 0 ? 'A' : 'B';
350
const getSnapshot = step === 0 ? getSnapshotA : getSnapshotB;
351
const value = useSyncExternalStore(store.subscribe, getSnapshot);
360
- return <Text text={label + value} />;
352
+ return React.createElement(Text, {
353
+ text: label + value,
354
+ });
355
}
362
-
356
let setStep;
357
function App() {
358
const [step, _setStep] = useState(0);
359
setStep = _setStep;
367
- return (
368
- <>
369
- <Child1 step={step} />
370
- <Child2 step={step} />
371
- </>
360
+ return React.createElement(
361
+ React.Fragment,
362
+ null,
363
+ React.createElement(Child1, {
364
+ step: step,
365
+ }),
366
+ React.createElement(Child2, {
367
+ step: step,
368
+ }),
369
);
370
}
374
-
371
const container = document.createElement('div');
372
const root = createRoot(container);
377
- await act(() => root.render(<App />));
373
+ await act(() => root.render(React.createElement(App, null)));
374
assertLog(['A1']);
375
expect(container.textContent).toEqual('A1');
380
-
376
await act(() => {
377
// Change getSnapshot and update the store in the same batch
378
setStep(1);
@@ -391,13 +386,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
386
]);
387
expect(container.textContent).toEqual('B2');
388
});
394
-
389
it('mutating the store in between render and commit when getSnapshot has _not_ changed', async () => {
390
// Same as previous test, but `getSnapshot` does not change
397
- const store = createExternalStore({a: 1, b: 1});
398
-
391
+ const store = createExternalStore({
392
+ a: 1,
393
+ b: 1,
394
+ });
395
const getSnapshotA = () => store.getState().a;
400
-
396
function Child1({step}) {
397
const value = useSyncExternalStore(store.subscribe, store.getState);
398
useLayoutEffect(() => {
@@ -407,32 +402,38 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
402
// fired yet, so it doesn't have access to the latest getSnapshot. So
403
// it can't use the getSnapshot to bail out.
404
Scheduler.log('Update B in commit phase');
410
- store.set({a: value.a, b: 2});
405
+ store.set({
406
+ a: value.a,
407
+ b: 2,
408
+ });
409
}
410
}, [step]);
411
return null;
412
}
415
-
413
function Child2({step}) {
414
const value = useSyncExternalStore(store.subscribe, getSnapshotA);
418
- return <Text text={'A' + value} />;
415
+ return React.createElement(Text, {
416
+ text: 'A' + value,
417
+ });
418
}
420
-
419
let setStep;
420
function App() {
421
const [step, _setStep] = useState(0);
422
setStep = _setStep;
425
- return (
426
- <>
427
- <Child1 step={step} />
428
- <Child2 step={step} />
429
- </>
423
+ return React.createElement(
424
+ React.Fragment,
425
+ null,
426
+ React.createElement(Child1, {
427
+ step: step,
428
+ }),
429
+ React.createElement(Child2, {
430
+ step: step,
431
+ }),
432
);
433
}
432
-
434
const container = document.createElement('div');
435
const root = createRoot(container);
435
- await act(() => root.render(<App />));
436
+ await act(() => root.render(React.createElement(App, null)));
437
assertLog(['A1']);
438
expect(container.textContent).toEqual('A1');
439
@@ -450,10 +451,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
451
]);
452
expect(container.textContent).toEqual('A1');
453
});
453
-
454
it("does not bail out if the previous update hasn't finished yet", async () => {
455
const store = createExternalStore(0);
456
-
456
function Child1() {
457
const value = useSyncExternalStore(store.subscribe, store.getState);
458
useLayoutEffect(() => {
@@ -462,85 +461,95 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
461
store.set(0);
462
}
463
}, [value]);
465
- return <Text text={value} />;
464
+ return React.createElement(Text, {
465
+ text: value,
466
+ });
467
}
467
-
468
function Child2() {
469
const value = useSyncExternalStore(store.subscribe, store.getState);
470
- return <Text text={value} />;
470
+ return React.createElement(Text, {
471
+ text: value,
472
+ });
473
}
472
-
474
const container = document.createElement('div');
475
const root = createRoot(container);
476
await act(() =>
477
root.render(
477
- <>
478
- <Child1 />
479
- <Child2 />
480
- </>,
478
+ React.createElement(
479
+ React.Fragment,
480
+ null,
481
+ React.createElement(Child1, null),
482
+ React.createElement(Child2, null),
483
+ ),
484
),
485
);
486
assertLog([0, 0]);
487
expect(container.textContent).toEqual('00');
485
-
488
await act(() => {
489
store.set(1);
490
});
491
assertLog([1, 1, 'Reset back to 0', 0, 0]);
492
expect(container.textContent).toEqual('00');
493
});
492
-
494
it('uses the latest getSnapshot, even if it changed in the same batch as a store update', async () => {
494
- const store = createExternalStore({a: 0, b: 0});
495
-
495
+ const store = createExternalStore({
496
+ a: 0,
497
+ b: 0,
498
+ });
499
const getSnapshotA = () => store.getState().a;
500
const getSnapshotB = () => store.getState().b;
498
-
501
let setGetSnapshot;
502
function App() {
503
const [getSnapshot, _setGetSnapshot] = useState(() => getSnapshotA);
504
setGetSnapshot = _setGetSnapshot;
505
const text = useSyncExternalStore(store.subscribe, getSnapshot);
504
- return <Text text={text} />;
506
+ return React.createElement(Text, {
507
+ text: text,
508
+ });
509
}
506
-
510
const container = document.createElement('div');
511
const root = createRoot(container);
509
- await act(() => root.render(<App />));
512
+ await act(() => root.render(React.createElement(App, null)));
513
assertLog([0]);
514
515
// Update the store and getSnapshot at the same time
516
await act(() => {
517
ReactDOM.flushSync(() => {
518
setGetSnapshot(() => getSnapshotB);
516
- store.set({a: 1, b: 2});
519
+ store.set({
520
+ a: 1,
521
+ b: 2,
522
+ });
523
});
524
});
525
// It should read from B instead of A
526
assertLog([2]);
527
expect(container.textContent).toEqual('2');
528
});
523
-
529
it('handles errors thrown by getSnapshot', async () => {
530
class ErrorBoundary extends React.Component {
526
- state = {error: null};
531
+ state = {
532
+ error: null,
533
+ };
534
static getDerivedStateFromError(error) {
528
- return {error};
535
+ return {
536
+ error,
537
+ };
538
}
539
render() {
540
if (this.state.error) {
532
- return <Text text={this.state.error.message} />;
541
+ return React.createElement(Text, {
542
+ text: this.state.error.message,
543
+ });
544
}
545
return this.props.children;
546
}
547
}
537
-
548
const store = createExternalStore({
549
value: 0,
550
throwInGetSnapshot: false,
551
throwInIsEqual: false,
552
});
543
-
553
function App() {
554
const {value} = useSyncExternalStore(store.subscribe, () => {
555
const state = store.getState();
@@ -549,17 +558,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
558
}
559
return state;
560
});
552
- return <Text text={value} />;
561
+ return React.createElement(Text, {
562
+ text: value,
563
+ });
564
}
554
-
565
const errorBoundary = React.createRef(null);
566
const container = document.createElement('div');
567
const root = createRoot(container);
568
await act(() =>
569
root.render(
560
- <ErrorBoundary ref={errorBoundary}>
561
- <App />
562
- </ErrorBoundary>,
570
+ React.createElement(
571
+ ErrorBoundary,
572
+ {
573
+ ref: errorBoundary,
574
+ },
575
+ React.createElement(App, null),
576
+ ),
577
),
578
);
579
assertLog([0]);
@@ -586,7 +600,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
600
});
601
});
602
}
589
-
603
assertLog(
604
gate(flags => flags.enableUseSyncExternalStoreShim)
605
? ['Error in getSnapshot']
@@ -599,22 +612,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
612
);
613
expect(container.textContent).toEqual('Error in getSnapshot');
614
});
602
-
615
it('Infinite loop if getSnapshot keeps returning new reference', async () => {
616
const store = createExternalStore({});
605
-
617
function App() {
618
const text = useSyncExternalStore(store.subscribe, () => ({}));
608
- return <Text text={JSON.stringify(text)} />;
619
+ return React.createElement(Text, {
620
+ text: JSON.stringify(text),
621
+ });
622
}
610
-
623
const container = document.createElement('div');
624
const root = createRoot(container);
613
-
625
await expect(async () => {
626
await expect(async () => {
627
await act(() => {
617
- ReactDOM.flushSync(async () => root.render(<App />));
628
+ ReactDOM.flushSync(async () =>
629
+ root.render(React.createElement(App, null)),
630
+ );
631
});
632
}).rejects.toThrow(
633
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
@@ -642,23 +655,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
655
},
656
);
657
});
645
-
658
it('getSnapshot can return NaN without infinite loop warning', async () => {
659
const store = createExternalStore('not a number');
648
-
660
function App() {
661
const value = useSyncExternalStore(store.subscribe, () =>
662
parseInt(store.getState(), 10),
663
);
653
- return <Text text={value} />;
664
+ return React.createElement(Text, {
665
+ text: value,
666
+ });
667
}
655
-
668
const container = document.createElement('div');
669
const root = createRoot(container);
670
671
// Initial render that reads a snapshot of NaN. This is OK because we use
672
// Object.is algorithm to compare values.
661
- await act(() => root.render(<App />));
673
+ await act(() => root.render(React.createElement(App, null)));
674
expect(container.textContent).toEqual('NaN');
675
assertLog([NaN]);
676
@@ -672,16 +684,16 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
684
expect(container.textContent).toEqual('NaN');
685
assertLog([NaN]);
686
});
675
-
687
describe('extra features implemented in user-space', () => {
688
it('memoized selectors are only called once per update', async () => {
678
- const store = createExternalStore({a: 0, b: 0});
679
-
689
+ const store = createExternalStore({
690
+ a: 0,
691
+ b: 0,
692
+ });
693
function selector(state) {
694
Scheduler.log('Selector');
695
return state.a;
696
}
684
-
697
function App() {
698
Scheduler.log('App');
699
const a = useSyncExternalStoreWithSelector(
@@ -690,19 +702,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
702
null,
703
selector,
704
);
693
- return <Text text={'A' + a} />;
705
+ return React.createElement(Text, {
706
+ text: 'A' + a,
707
+ });
708
}
695
-
709
const container = document.createElement('div');
710
const root = createRoot(container);
698
- await act(() => root.render(<App />));
699
-
711
+ await act(() => root.render(React.createElement(App, null)));
712
assertLog(['App', 'Selector', 'A0']);
713
expect(container.textContent).toEqual('A0');
714
715
// Update the store
716
await act(() => {
705
- store.set({a: 1, b: 0});
717
+ store.set({
718
+ a: 1,
719
+ b: 0,
720
+ });
721
});
722
assertLog([
723
// The selector runs before React starts rendering
@@ -714,19 +729,24 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
729
]);
730
expect(container.textContent).toEqual('A1');
731
});
717
-
732
it('Using isEqual to bailout', async () => {
719
- const store = createExternalStore({a: 0, b: 0});
720
-
733
+ const store = createExternalStore({
734
+ a: 0,
735
+ b: 0,
736
+ });
737
function A() {
738
const {a} = useSyncExternalStoreWithSelector(
739
store.subscribe,
740
store.getState,
741
null,
726
- state => ({a: state.a}),
742
+ state => ({
743
+ a: state.a,
744
+ }),
745
(state1, state2) => state1.a === state2.a,
746
);
729
- return <Text text={'A' + a} />;
747
+ return React.createElement(Text, {
748
+ text: 'A' + a,
749
+ });
750
}
751
function B() {
752
const {b} = useSyncExternalStoreWithSelector(
@@ -734,32 +754,36 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
754
store.getState,
755
null,
756
state => {
737
- return {b: state.b};
757
+ return {
758
+ b: state.b,
759
+ };
760
},
761
(state1, state2) => state1.b === state2.b,
762
);
741
- return <Text text={'B' + b} />;
763
+ return React.createElement(Text, {
764
+ text: 'B' + b,
765
+ });
766
}
743
-
767
function App() {
745
- return (
746
- <>
747
- <A />
748
- <B />
749
- </>
768
+ return React.createElement(
769
+ React.Fragment,
770
+ null,
771
+ React.createElement(A, null),
772
+ React.createElement(B, null),
773
);
774
}
752
-
775
const container = document.createElement('div');
776
const root = createRoot(container);
755
- await act(() => root.render(<App />));
756
-
777
+ await act(() => root.render(React.createElement(App, null)));
778
assertLog(['A0', 'B0']);
779
expect(container.textContent).toEqual('A0B0');
780
781
// Update b but not a
782
await act(() => {
762
- store.set({a: 0, b: 1});
783
+ store.set({
784
+ a: 0,
785
+ b: 1,
786
+ });
787
});
788
// Only b re-renders
789
assertLog(['B1']);
@@ -767,16 +791,17 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
791
792
// Update a but not b
793
await act(() => {
770
- store.set({a: 1, b: 1});
794
+ store.set({
795
+ a: 1,
796
+ b: 1,
797
+ });
798
});
799
// Only a re-renders
800
assertLog(['A1']);
801
expect(container.textContent).toEqual('A1B1');
802
});
776
-
803
it('basic server hydration', async () => {
804
const store = createExternalStore('client');
779
-
805
const ref = React.createRef();
806
function App() {
807
const text = useSyncExternalStore(
@@ -787,20 +812,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
812
useEffect(() => {
813
Scheduler.log('Passive effect: ' + text);
814
}, [text]);
790
- return (
791
- <div ref={ref}>
792
- <Text text={text} />
793
- </div>
815
+ return React.createElement(
816
+ 'div',
817
+ {
818
+ ref: ref,
819
+ },
820
+ React.createElement(Text, {
821
+ text: text,
822
+ }),
823
);
824
}
796
-
825
const container = document.createElement('div');
826
container.innerHTML = '<div>server</div>';
827
const serverRenderedDiv = container.getElementsByTagName('div')[0];
800
-
828
if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
829
await act(() => {
803
- ReactDOMClient.hydrateRoot(container, <App />);
830
+ ReactDOMClient.hydrateRoot(container, React.createElement(App, null));
831
});
832
assertLog([
833
// First it hydrates the server rendered HTML
@@ -816,9 +843,9 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
843
// client. To avoid this server mismatch warning, user must account for
844
// this themselves and return the correct value inside `getSnapshot`.
845
await act(() => {
819
- expect(() => ReactDOM.hydrate(<App />, container)).toErrorDev(
820
- 'Text content did not match',
821
- );
846
+ expect(() =>
847
+ ReactDOM.hydrate(React.createElement(App, null), container),
848
+ ).toErrorDev('Text content did not match');
849
});
850
assertLog(['client', 'Passive effect: client']);
851
}
@@ -826,10 +853,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
853
expect(ref.current).toEqual(serverRenderedDiv);
854
});
855
});
829
-
856
it('regression test for #23150', async () => {
857
const store = createExternalStore('Initial');
832
-
858
function App() {
859
const text = useSyncExternalStore(store.subscribe, store.getState);
860
const [derivedText, setDerivedText] = useState(text);
@@ -837,26 +862,25 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
862
if (derivedText !== text.toUpperCase()) {
863
setDerivedText(text.toUpperCase());
864
}
840
- return <Text text={derivedText} />;
865
+ return React.createElement(Text, {
866
+ text: derivedText,
867
+ });
868
}
842
-
869
const container = document.createElement('div');
870
const root = createRoot(container);
845
- await act(() => root.render(<App />));
846
-
871
+ await act(() => root.render(React.createElement(App, null)));
872
assertLog(['INITIAL']);
873
expect(container.textContent).toEqual('INITIAL');
849
-
874
await act(() => {
875
store.set('Updated');
876
});
877
assertLog(['UPDATED']);
878
expect(container.textContent).toEqual('UPDATED');
879
});
856
-
880
it('compares selection to rendered selection even if selector changes', async () => {
858
- const store = createExternalStore({items: ['A', 'B']});
859
-
881
+ const store = createExternalStore({
882
+ items: ['A', 'B'],
883
+ });
884
const shallowEqualArray = (a, b) => {
885
if (a.length !== b.length) {
886
return false;
@@ -868,19 +892,24 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
892
}
893
return true;
894
};
871
-
895
const List = React.memo(({items}) => {
873
- return (
874
- <ul>
875
- {items.map(text => (
876
- <li key={text}>
877
- <Text key={text} text={text} />
878
- </li>
879
- ))}
880
- </ul>
896
+ return React.createElement(
897
+ 'ul',
898
+ null,
899
+ items.map(text =>
900
+ React.createElement(
901
+ 'li',
902
+ {
903
+ key: text,
904
+ },
905
+ React.createElement(Text, {
906
+ key: text,
907
+ text: text,
908
+ }),
909
+ ),
910
+ ),
911
);
912
});
883
-
913
function App({step}) {
914
const inlineSelector = state => {
915
Scheduler.log('Inline selector');
@@ -893,28 +922,37 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
922
inlineSelector,
923
shallowEqualArray,
924
);
896
- return (
897
- <>
898
- <List items={items} />
899
- <Text text={'Sibling: ' + step} />
900
- </>
925
+ return React.createElement(
926
+ React.Fragment,
927
+ null,
928
+ React.createElement(List, {
929
+ items: items,
930
+ }),
931
+ React.createElement(Text, {
932
+ text: 'Sibling: ' + step,
933
+ }),
934
);
935
}
903
-
936
const container = document.createElement('div');
937
const root = createRoot(container);
938
await act(() => {
907
- root.render(<App step={0} />);
939
+ root.render(
940
+ React.createElement(App, {
941
+ step: 0,
942
+ }),
943
+ );
944
});
945
assertLog(['Inline selector', 'A', 'B', 'C', 'Sibling: 0']);
910
-
946
await act(() => {
912
- root.render(<App step={1} />);
947
+ root.render(
948
+ React.createElement(App, {
949
+ step: 1,
950
+ }),
951
+ );
952
});
953
assertLog([
954
// We had to call the selector again because it's not memoized
955
'Inline selector',
917
-
956
// But because the result was the same (according to isEqual) we can
957
// bail out of rendering the memoized list. These are skipped:
958
// 'A',
@@ -924,33 +962,38 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
962
'Sibling: 1',
963
]);
964
});
927
-
965
describe('selector and isEqual error handling in extra', () => {
966
let ErrorBoundary;
967
beforeEach(() => {
968
ErrorBoundary = class extends React.Component {
932
- state = {error: null};
969
+ state = {
970
+ error: null,
971
+ };
972
static getDerivedStateFromError(error) {
934
- return {error};
973
+ return {
974
+ error,
975
+ };
976
}
977
render() {
978
if (this.state.error) {
938
- return <Text text={this.state.error.message} />;
979
+ return React.createElement(Text, {
980
+ text: this.state.error.message,
981
+ });
982
}
983
return this.props.children;
984
}
985
};
986
});
944
-
987
it('selector can throw on update', async () => {
946
- const store = createExternalStore({a: 'a'});
988
+ const store = createExternalStore({
989
+ a: 'a',
990
+ });
991
const selector = state => {
992
if (typeof state.a !== 'string') {
993
throw new TypeError('Malformed state');
994
}
995
return state.a.toUpperCase();
996
};
953
-
997
function App() {
998
const a = useSyncExternalStoreWithSelector(
999
store.subscribe,
@@ -958,22 +1001,23 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
1001
null,
1002
selector,
1003
);
961
- return <Text text={a} />;
1004
+ return React.createElement(Text, {
1005
+ text: a,
1006
+ });
1007
}
963
-
1008
const container = document.createElement('div');
1009
const root = createRoot(container);
1010
await act(() =>
1011
root.render(
968
- <ErrorBoundary>
969
- <App />
970
- </ErrorBoundary>,
1012
+ React.createElement(
1013
+ ErrorBoundary,
1014
+ null,
1015
+ React.createElement(App, null),
1016
+ ),
1017
),
1018
);
973
-
1019
assertLog(['A']);
1020
expect(container.textContent).toEqual('A');
976
-
1021
if (__DEV__ && gate(flags => flags.enableUseSyncExternalStoreShim)) {
1022
// In 17, the error is re-thrown in DEV.
1023
await expect(async () => {
@@ -986,12 +1030,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
1030
store.set({});
1031
});
1032
}
989
-
1033
expect(container.textContent).toEqual('Malformed state');
1034
});
992
-
1035
it('isEqual can throw on update', async () => {
994
- const store = createExternalStore({a: 'A'});
1036
+ const store = createExternalStore({
1037
+ a: 'A',
1038
+ });
1039
const selector = state => state.a;
1040
const isEqual = (left, right) => {
1041
if (typeof left.a !== 'string' || typeof right.a !== 'string') {
@@ -999,7 +1043,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
1043
}
1044
return left.a.trim() === right.a.trim();
1045
};
1002
-
1046
function App() {
1047
const a = useSyncExternalStoreWithSelector(
1048
store.subscribe,
@@ -1008,22 +1051,23 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
1051
selector,
1052
isEqual,
1053
);
1011
- return <Text text={a} />;
1054
+ return React.createElement(Text, {
1055
+ text: a,
1056
+ });
1057
}
1013
-
1058
const container = document.createElement('div');
1059
const root = createRoot(container);
1060
await act(() =>
1061
root.render(
1018
- <ErrorBoundary>
1019
- <App />
1020
- </ErrorBoundary>,
1062
+ React.createElement(
1063
+ ErrorBoundary,
1064
+ null,
1065
+ React.createElement(App, null),
1066
+ ),
1067
),
1068
);
1023
-
1069
assertLog(['A']);
1070
expect(container.textContent).toEqual('A');
1026
-
1071
if (__DEV__ && gate(flags => flags.enableUseSyncExternalStoreShim)) {
1072
// In 17, the error is re-thrown in DEV.
1073
await expect(async () => {
@@ -1036,7 +1080,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
1080
store.set({});
1081
});
1082
}
1039
-
1083
expect(container.textContent).toEqual('Malformed state');
1084
});
1085
});
scripts/error-codes/codes.json
+2
-1
@@ -509,5 +509,6 @@
509
"521": "flushSyncWork should not be called from builds that support legacy mode. This is a bug in React.",
510
"522": "Invalid form element. requestFormReset must be passed a form that was rendered by React.",
511
"523": "The render was aborted due to being postponed.",
512
- "524": "Values cannot be passed to next() of AsyncIterables passed to Client Components."
512
+ "524": "Values cannot be passed to next() of AsyncIterables passed to Client Components.",
513
+ "525": "A React Element from an older version of React was rendered. This is not supported. It can happen if:\n- Multiple copies of the \"react\" package is used.\n- A library pre-bundled an old copy of \"react\" or \"react/jsx-runtime\".\n- A compiler tries to \"inline\" JSX instead of using the runtime."
514
}