@samitouri / QOS-React / commits / 3b551c8284

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 }