diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
index 10a355d0430f3..f05c832c56c15 100644
--- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
+++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
@@ -290,9 +290,23 @@ describe('InspectedElementContext', () => {
"preview_long": {boolean: true, number: 123, string: "abc"},
},
},
- "react_element": Dehydrated {
- "preview_short": ,
- "preview_long": ,
+ "react_element": {
+ "$$typeof": Dehydrated {
+ "preview_short": Symbol(react.element),
+ "preview_long": Symbol(react.element),
+ },
+ "_owner": null,
+ "_store": Dehydrated {
+ "preview_short": {…},
+ "preview_long": {},
+ },
+ "key": null,
+ "props": Dehydrated {
+ "preview_short": {…},
+ "preview_long": {},
+ },
+ "ref": null,
+ "type": "span",
},
"regexp": Dehydrated {
"preview_short": /abc/giu,
diff --git a/packages/react-devtools-shared/src/backend/ReactSymbols.js b/packages/react-devtools-shared/src/backend/ReactSymbols.js
index 35e64abbeded0..7a7a9c107e93f 100644
--- a/packages/react-devtools-shared/src/backend/ReactSymbols.js
+++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js
@@ -23,8 +23,9 @@ export const SERVER_CONTEXT_SYMBOL_STRING = 'Symbol(react.server_context)';
export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)';
-export const ELEMENT_NUMBER = 0xeac7;
-export const ELEMENT_SYMBOL_STRING = 'Symbol(react.element)';
+export const ELEMENT_SYMBOL_STRING = 'Symbol(react.transitional.element)';
+export const LEGACY_ELEMENT_NUMBER = 0xeac7;
+export const LEGACY_ELEMENT_SYMBOL_STRING = 'Symbol(react.element)';
export const DEBUG_TRACING_MODE_NUMBER = 0xeae1;
export const DEBUG_TRACING_MODE_SYMBOL_STRING =
diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js
index 678a7ce4f6db9..61538fed69492 100644
--- a/packages/react-dom/src/__tests__/ReactComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactComponent-test.js
@@ -612,6 +612,32 @@ describe('ReactComponent', () => {
);
});
+ // @gate renameElementSymbol
+ it('throws if a legacy element is used as a child', async () => {
+ const inlinedElement = {
+ $$typeof: Symbol.for('react.element'),
+ type: 'div',
+ key: null,
+ ref: null,
+ props: {},
+ _owner: null,
+ };
+ const element =
{[inlinedElement]}
;
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await expect(
+ act(() => {
+ root.render(element);
+ }),
+ ).rejects.toThrowError(
+ '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.',
+ );
+ });
+
it('throws if a plain object even if it is in an owner', async () => {
class Foo extends React.Component {
render() {
diff --git a/packages/react-dom/src/__tests__/ReactDOMOption-test.js b/packages/react-dom/src/__tests__/ReactDOMOption-test.js
index 1661a98b1a694..4b431a85bbdee 100644
--- a/packages/react-dom/src/__tests__/ReactDOMOption-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMOption-test.js
@@ -134,6 +134,7 @@ describe('ReactDOMOption', () => {
}).rejects.toThrow('Objects are not valid as a React child');
});
+ // @gate www
it('should support element-ish child', async () => {
// This is similar to .
// We don't toString it because you must instead provide a value prop.
diff --git a/packages/react-dom/src/__tests__/refs-test.js b/packages/react-dom/src/__tests__/refs-test.js
index 577d91f38f368..0c47e1728b270 100644
--- a/packages/react-dom/src/__tests__/refs-test.js
+++ b/packages/react-dom/src/__tests__/refs-test.js
@@ -382,7 +382,7 @@ describe('ref swapping', () => {
}).rejects.toThrow('Expected ref to be a function');
});
- // @gate !enableRefAsProp
+ // @gate !enableRefAsProp && www
it('undefined ref on manually inlined React element triggers error', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index f0fe8883b9eb4..b11e8b59ad1eb 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -32,6 +32,7 @@ import {
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
REACT_CONTEXT_TYPE,
+ REACT_LEGACY_ELEMENT_TYPE,
} from 'shared/ReactSymbols';
import {
HostRoot,
@@ -166,6 +167,16 @@ function coerceRef(
}
function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
+ if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) {
+ throw new Error(
+ '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.',
+ );
+ }
+
// $FlowFixMe[method-unbinding]
const childString = Object.prototype.toString.call(newChild);
diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js
index 36185639e67f0..9fea14ae5cf7b 100644
--- a/packages/react/src/jsx/ReactJSXElement.js
+++ b/packages/react/src/jsx/ReactJSXElement.js
@@ -162,7 +162,7 @@ function elementRefGetterWithDeprecationWarning() {
/**
* Factory method to create a new React element. This no longer adheres to
* the class pattern, so do not use new to call it. Also, instanceof check
- * will not work. Instead test $$typeof field against Symbol.for('react.element') to check
+ * will not work. Instead test $$typeof field against Symbol.for('react.transitional.element') to check
* if something is a React Element.
*
* @param {*} type
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 5e987aec180e3..545afd1cdcc32 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -143,6 +143,9 @@ export const transitionLaneExpirationMs = 5000;
// const __NEXT_MAJOR__ = __EXPERIMENTAL__;
+// Renames the internal symbol for elements since they have changed signature/constructor
+export const renameElementSymbol = true;
+
// Removes legacy style context
export const disableLegacyContext = true;
diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js
index 26662aa325e7c..002870896f00f 100644
--- a/packages/shared/ReactSymbols.js
+++ b/packages/shared/ReactSymbols.js
@@ -7,12 +7,17 @@
* @flow
*/
+import {renameElementSymbol} from 'shared/ReactFeatureFlags';
+
// ATTENTION
// When adding new symbols to this file,
// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'
// The Symbol used to tag the ReactElement-like types.
-export const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.element');
+export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element');
+export const REACT_ELEMENT_TYPE: symbol = renameElementSymbol
+ ? Symbol.for('react.transitional.element')
+ : REACT_LEGACY_ELEMENT_TYPE;
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
export const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode');
diff --git a/packages/shared/__tests__/ReactSymbols-test.internal.js b/packages/shared/__tests__/ReactSymbols-test.internal.js
index c651f53075a12..2ee93336ea7ee 100644
--- a/packages/shared/__tests__/ReactSymbols-test.internal.js
+++ b/packages/shared/__tests__/ReactSymbols-test.internal.js
@@ -23,6 +23,7 @@ describe('ReactSymbols', () => {
});
};
+ // @gate renameElementSymbol
it('Symbol values should be unique', () => {
expectToBeUnique(Object.entries(require('shared/ReactSymbols')));
});
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index b0df95c2d6bb3..a4d0434f50469 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -69,6 +69,8 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = true;
export const enableGetInspectorDataForInstanceInProduction = true;
+export const renameElementSymbol = false;
+
export const enableRetryLaneExpiration = false;
export const retryLaneExpirationMs = 5000;
export const syncLaneExpirationMs = 250;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index defe3d6e0fe88..723a6a69c376b 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -104,6 +104,8 @@ export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
export const passChildrenWhenCloningPersistedNodes = false;
export const enableEarlyReturnForPropDiffing = false;
+export const renameElementSymbol = true;
+
// Profiling Only
export const enableProfilerTimer = __PROFILE__;
export const enableProfilerCommitHooks = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 26b4086ca19fe..2271d9b2a25db 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -79,6 +79,8 @@ export const enableServerComponentLogs = true;
export const enableInfiniteRenderLoopDetection = false;
export const enableEarlyReturnForPropDiffing = false;
+export const renameElementSymbol = true;
+
// TODO: This must be in sync with the main ReactFeatureFlags file because
// the Test Renderer's value must be the same as the one used by the
// react package.
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index f39974ab98c98..e6b299035a99e 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -90,5 +90,7 @@ export const disableDOMTestUtils = false;
export const disableDefaultPropsExceptForClasses = false;
export const enableEarlyReturnForPropDiffing = false;
+export const renameElementSymbol = false;
+
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index fdb85b0be0e67..44972804eb2de 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -90,5 +90,7 @@ export const disableDOMTestUtils = false;
export const disableDefaultPropsExceptForClasses = false;
export const enableEarlyReturnForPropDiffing = false;
+export const renameElementSymbol = false;
+
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 753d2f27b67d5..cf634cb168700 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -65,6 +65,8 @@ export const enableSchedulingProfiler: boolean =
export const disableLegacyContext = __EXPERIMENTAL__;
export const enableGetInspectorDataForInstanceInProduction = false;
+export const renameElementSymbol = false;
+
export const enableCache = true;
export const enableLegacyCache = true;
export const enableFetchInstrumentation = false;
diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
index e01ef57c10a64..8f8fb8e9a9a5a 100644
--- a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
@@ -20,7 +20,6 @@ let useState;
let useEffect;
let useLayoutEffect;
let assertLog;
-
let originalError;
// This tests shared behavior between the built-in and shim implementations of
@@ -28,7 +27,6 @@ let originalError;
describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
beforeEach(() => {
jest.resetModules();
-
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
// Test the shim against React 17.
jest.mock('react', () => {
@@ -49,7 +47,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
originalError = console.error;
console.error = jest.fn();
}
-
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
@@ -57,17 +54,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useState = React.useState;
useEffect = React.useEffect;
useLayoutEffect = React.useLayoutEffect;
-
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
-
const internalAct = require('internal-test-utils').act;
// The internal act implementation doesn't batch updates by default, since
// it's mostly used to test concurrent mode. But since these tests run
// in both concurrent and legacy mode, I'm adding batching here.
act = cb => internalAct(() => ReactDOM.unstable_batchedUpdates(cb));
-
if (gate(flags => flags.source)) {
// The `shim/with-selector` module composes the main
// `use-sync-external-store` entrypoint. In the compiled artifacts, this
@@ -84,18 +78,15 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useSyncExternalStoreWithSelector =
require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
});
-
afterEach(() => {
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
console.error = originalError;
}
});
-
function Text({text}) {
Scheduler.log(text);
return text;
}
-
function createRoot(container) {
// This wrapper function exists so we can test both legacy roots and
// concurrent roots.
@@ -117,7 +108,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
};
}
}
-
function createExternalStore(initialState) {
const listeners = new Set();
let currentState = initialState;
@@ -140,41 +130,36 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
},
};
}
-
it('basic usage', async () => {
const store = createExternalStore('Initial');
-
function App() {
const text = useSyncExternalStore(store.subscribe, store.getState);
- return ;
+ return React.createElement(Text, {
+ text: text,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['Initial']);
expect(container.textContent).toEqual('Initial');
-
await act(() => {
store.set('Updated');
});
assertLog(['Updated']);
expect(container.textContent).toEqual('Updated');
});
-
it('skips re-rendering if nothing changes', async () => {
const store = createExternalStore('Initial');
-
function App() {
const text = useSyncExternalStore(store.subscribe, store.getState);
- return ;
+ return React.createElement(Text, {
+ text: text,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['Initial']);
expect(container.textContent).toEqual('Initial');
@@ -186,26 +171,23 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
assertLog([]);
expect(container.textContent).toEqual('Initial');
});
-
it('switch to a different store', async () => {
const storeA = createExternalStore(0);
const storeB = createExternalStore(0);
-
let setStore;
function App() {
const [store, _setStore] = useState(storeA);
setStore = _setStore;
const value = useSyncExternalStore(store.subscribe, store.getState);
- return ;
+ return React.createElement(Text, {
+ text: value,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog([0]);
expect(container.textContent).toEqual('0');
-
await act(() => {
storeA.set(1);
});
@@ -239,38 +221,43 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
assertLog([1]);
expect(container.textContent).toEqual('1');
});
-
it('selecting a specific value inside getSnapshot', async () => {
- const store = createExternalStore({a: 0, b: 0});
-
+ const store = createExternalStore({
+ a: 0,
+ b: 0,
+ });
function A() {
const a = useSyncExternalStore(store.subscribe, () => store.getState().a);
- return ;
+ return React.createElement(Text, {
+ text: 'A' + a,
+ });
}
function B() {
const b = useSyncExternalStore(store.subscribe, () => store.getState().b);
- return ;
+ return React.createElement(Text, {
+ text: 'B' + b,
+ });
}
-
function App() {
- return (
- <>
-
-
- >
+ return React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(A, null),
+ React.createElement(B, null),
);
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['A0', 'B0']);
expect(container.textContent).toEqual('A0B0');
// Update b but not a
await act(() => {
- store.set({a: 0, b: 1});
+ store.set({
+ a: 0,
+ b: 1,
+ });
});
// Only b re-renders
assertLog(['B1']);
@@ -278,7 +265,10 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// Update a but not b
await act(() => {
- store.set({a: 1, b: 1});
+ store.set({
+ a: 1,
+ b: 1,
+ });
});
// Only a re-renders
assertLog(['A1']);
@@ -293,18 +283,18 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
'mutation in between the sync and passive effects',
async () => {
const store = createExternalStore(0);
-
function App() {
const value = useSyncExternalStore(store.subscribe, store.getState);
useEffect(() => {
Scheduler.log('Passive effect: ' + value);
}, [value]);
- return ;
+ return React.createElement(Text, {
+ text: value,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
+ await act(() => root.render(React.createElement(App, null)));
assertLog([0, 'Passive effect: 0']);
// 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)', () => {
expect(container.textContent).toEqual('0');
},
);
-
it('mutating the store in between render and commit when getSnapshot has changed', async () => {
- const store = createExternalStore({a: 1, b: 1});
-
+ const store = createExternalStore({
+ a: 1,
+ b: 1,
+ });
const getSnapshotA = () => store.getState().a;
const getSnapshotB = () => store.getState().b;
-
function Child1({step}) {
const value = useSyncExternalStore(store.subscribe, store.getState);
useLayoutEffect(() => {
@@ -347,37 +337,42 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// fired yet, so it doesn't have access to the latest getSnapshot. So
// it can't use the getSnapshot to bail out.
Scheduler.log('Update B in commit phase');
- store.set({a: value.a, b: 2});
+ store.set({
+ a: value.a,
+ b: 2,
+ });
}
}, [step]);
return null;
}
-
function Child2({step}) {
const label = step === 0 ? 'A' : 'B';
const getSnapshot = step === 0 ? getSnapshotA : getSnapshotB;
const value = useSyncExternalStore(store.subscribe, getSnapshot);
- return ;
+ return React.createElement(Text, {
+ text: label + value,
+ });
}
-
let setStep;
function App() {
const [step, _setStep] = useState(0);
setStep = _setStep;
- return (
- <>
-
-
- >
+ return React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(Child1, {
+ step: step,
+ }),
+ React.createElement(Child2, {
+ step: step,
+ }),
);
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['A1']);
expect(container.textContent).toEqual('A1');
-
await act(() => {
// Change getSnapshot and update the store in the same batch
setStep(1);
@@ -391,13 +386,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
]);
expect(container.textContent).toEqual('B2');
});
-
it('mutating the store in between render and commit when getSnapshot has _not_ changed', async () => {
// Same as previous test, but `getSnapshot` does not change
- const store = createExternalStore({a: 1, b: 1});
-
+ const store = createExternalStore({
+ a: 1,
+ b: 1,
+ });
const getSnapshotA = () => store.getState().a;
-
function Child1({step}) {
const value = useSyncExternalStore(store.subscribe, store.getState);
useLayoutEffect(() => {
@@ -407,32 +402,38 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// fired yet, so it doesn't have access to the latest getSnapshot. So
// it can't use the getSnapshot to bail out.
Scheduler.log('Update B in commit phase');
- store.set({a: value.a, b: 2});
+ store.set({
+ a: value.a,
+ b: 2,
+ });
}
}, [step]);
return null;
}
-
function Child2({step}) {
const value = useSyncExternalStore(store.subscribe, getSnapshotA);
- return ;
+ return React.createElement(Text, {
+ text: 'A' + value,
+ });
}
-
let setStep;
function App() {
const [step, _setStep] = useState(0);
setStep = _setStep;
- return (
- <>
-
-
- >
+ return React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(Child1, {
+ step: step,
+ }),
+ React.createElement(Child2, {
+ step: step,
+ }),
);
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['A1']);
expect(container.textContent).toEqual('A1');
@@ -450,10 +451,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
]);
expect(container.textContent).toEqual('A1');
});
-
it("does not bail out if the previous update hasn't finished yet", async () => {
const store = createExternalStore(0);
-
function Child1() {
const value = useSyncExternalStore(store.subscribe, store.getState);
useLayoutEffect(() => {
@@ -462,85 +461,95 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set(0);
}
}, [value]);
- return ;
+ return React.createElement(Text, {
+ text: value,
+ });
}
-
function Child2() {
const value = useSyncExternalStore(store.subscribe, store.getState);
- return ;
+ return React.createElement(Text, {
+ text: value,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
await act(() =>
root.render(
- <>
-
-
- >,
+ React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(Child1, null),
+ React.createElement(Child2, null),
+ ),
),
);
assertLog([0, 0]);
expect(container.textContent).toEqual('00');
-
await act(() => {
store.set(1);
});
assertLog([1, 1, 'Reset back to 0', 0, 0]);
expect(container.textContent).toEqual('00');
});
-
it('uses the latest getSnapshot, even if it changed in the same batch as a store update', async () => {
- const store = createExternalStore({a: 0, b: 0});
-
+ const store = createExternalStore({
+ a: 0,
+ b: 0,
+ });
const getSnapshotA = () => store.getState().a;
const getSnapshotB = () => store.getState().b;
-
let setGetSnapshot;
function App() {
const [getSnapshot, _setGetSnapshot] = useState(() => getSnapshotA);
setGetSnapshot = _setGetSnapshot;
const text = useSyncExternalStore(store.subscribe, getSnapshot);
- return ;
+ return React.createElement(Text, {
+ text: text,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
+ await act(() => root.render(React.createElement(App, null)));
assertLog([0]);
// Update the store and getSnapshot at the same time
await act(() => {
ReactDOM.flushSync(() => {
setGetSnapshot(() => getSnapshotB);
- store.set({a: 1, b: 2});
+ store.set({
+ a: 1,
+ b: 2,
+ });
});
});
// It should read from B instead of A
assertLog([2]);
expect(container.textContent).toEqual('2');
});
-
it('handles errors thrown by getSnapshot', async () => {
class ErrorBoundary extends React.Component {
- state = {error: null};
+ state = {
+ error: null,
+ };
static getDerivedStateFromError(error) {
- return {error};
+ return {
+ error,
+ };
}
render() {
if (this.state.error) {
- return ;
+ return React.createElement(Text, {
+ text: this.state.error.message,
+ });
}
return this.props.children;
}
}
-
const store = createExternalStore({
value: 0,
throwInGetSnapshot: false,
throwInIsEqual: false,
});
-
function App() {
const {value} = useSyncExternalStore(store.subscribe, () => {
const state = store.getState();
@@ -549,17 +558,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
}
return state;
});
- return ;
+ return React.createElement(Text, {
+ text: value,
+ });
}
-
const errorBoundary = React.createRef(null);
const container = document.createElement('div');
const root = createRoot(container);
await act(() =>
root.render(
-
-
- ,
+ React.createElement(
+ ErrorBoundary,
+ {
+ ref: errorBoundary,
+ },
+ React.createElement(App, null),
+ ),
),
);
assertLog([0]);
@@ -586,7 +600,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
});
});
}
-
assertLog(
gate(flags => flags.enableUseSyncExternalStoreShim)
? ['Error in getSnapshot']
@@ -599,22 +612,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
);
expect(container.textContent).toEqual('Error in getSnapshot');
});
-
it('Infinite loop if getSnapshot keeps returning new reference', async () => {
const store = createExternalStore({});
-
function App() {
const text = useSyncExternalStore(store.subscribe, () => ({}));
- return ;
+ return React.createElement(Text, {
+ text: JSON.stringify(text),
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
-
await expect(async () => {
await expect(async () => {
await act(() => {
- ReactDOM.flushSync(async () => root.render());
+ ReactDOM.flushSync(async () =>
+ root.render(React.createElement(App, null)),
+ );
});
}).rejects.toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
@@ -642,23 +655,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
},
);
});
-
it('getSnapshot can return NaN without infinite loop warning', async () => {
const store = createExternalStore('not a number');
-
function App() {
const value = useSyncExternalStore(store.subscribe, () =>
parseInt(store.getState(), 10),
);
- return ;
+ return React.createElement(Text, {
+ text: value,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
// Initial render that reads a snapshot of NaN. This is OK because we use
// Object.is algorithm to compare values.
- await act(() => root.render());
+ await act(() => root.render(React.createElement(App, null)));
expect(container.textContent).toEqual('NaN');
assertLog([NaN]);
@@ -672,16 +684,16 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(container.textContent).toEqual('NaN');
assertLog([NaN]);
});
-
describe('extra features implemented in user-space', () => {
it('memoized selectors are only called once per update', async () => {
- const store = createExternalStore({a: 0, b: 0});
-
+ const store = createExternalStore({
+ a: 0,
+ b: 0,
+ });
function selector(state) {
Scheduler.log('Selector');
return state.a;
}
-
function App() {
Scheduler.log('App');
const a = useSyncExternalStoreWithSelector(
@@ -690,19 +702,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
null,
selector,
);
- return ;
+ return React.createElement(Text, {
+ text: 'A' + a,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['App', 'Selector', 'A0']);
expect(container.textContent).toEqual('A0');
// Update the store
await act(() => {
- store.set({a: 1, b: 0});
+ store.set({
+ a: 1,
+ b: 0,
+ });
});
assertLog([
// The selector runs before React starts rendering
@@ -714,19 +729,24 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
]);
expect(container.textContent).toEqual('A1');
});
-
it('Using isEqual to bailout', async () => {
- const store = createExternalStore({a: 0, b: 0});
-
+ const store = createExternalStore({
+ a: 0,
+ b: 0,
+ });
function A() {
const {a} = useSyncExternalStoreWithSelector(
store.subscribe,
store.getState,
null,
- state => ({a: state.a}),
+ state => ({
+ a: state.a,
+ }),
(state1, state2) => state1.a === state2.a,
);
- return ;
+ return React.createElement(Text, {
+ text: 'A' + a,
+ });
}
function B() {
const {b} = useSyncExternalStoreWithSelector(
@@ -734,32 +754,36 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.getState,
null,
state => {
- return {b: state.b};
+ return {
+ b: state.b,
+ };
},
(state1, state2) => state1.b === state2.b,
);
- return ;
+ return React.createElement(Text, {
+ text: 'B' + b,
+ });
}
-
function App() {
- return (
- <>
-
-
- >
+ return React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(A, null),
+ React.createElement(B, null),
);
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['A0', 'B0']);
expect(container.textContent).toEqual('A0B0');
// Update b but not a
await act(() => {
- store.set({a: 0, b: 1});
+ store.set({
+ a: 0,
+ b: 1,
+ });
});
// Only b re-renders
assertLog(['B1']);
@@ -767,16 +791,17 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// Update a but not b
await act(() => {
- store.set({a: 1, b: 1});
+ store.set({
+ a: 1,
+ b: 1,
+ });
});
// Only a re-renders
assertLog(['A1']);
expect(container.textContent).toEqual('A1B1');
});
-
it('basic server hydration', async () => {
const store = createExternalStore('client');
-
const ref = React.createRef();
function App() {
const text = useSyncExternalStore(
@@ -787,20 +812,22 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useEffect(() => {
Scheduler.log('Passive effect: ' + text);
}, [text]);
- return (
-
-
-
+ return React.createElement(
+ 'div',
+ {
+ ref: ref,
+ },
+ React.createElement(Text, {
+ text: text,
+ }),
);
}
-
const container = document.createElement('div');
container.innerHTML = 'server
';
const serverRenderedDiv = container.getElementsByTagName('div')[0];
-
if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
await act(() => {
- ReactDOMClient.hydrateRoot(container, );
+ ReactDOMClient.hydrateRoot(container, React.createElement(App, null));
});
assertLog([
// First it hydrates the server rendered HTML
@@ -816,9 +843,9 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// client. To avoid this server mismatch warning, user must account for
// this themselves and return the correct value inside `getSnapshot`.
await act(() => {
- expect(() => ReactDOM.hydrate(, container)).toErrorDev(
- 'Text content did not match',
- );
+ expect(() =>
+ ReactDOM.hydrate(React.createElement(App, null), container),
+ ).toErrorDev('Text content did not match');
});
assertLog(['client', 'Passive effect: client']);
}
@@ -826,10 +853,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(ref.current).toEqual(serverRenderedDiv);
});
});
-
it('regression test for #23150', async () => {
const store = createExternalStore('Initial');
-
function App() {
const text = useSyncExternalStore(store.subscribe, store.getState);
const [derivedText, setDerivedText] = useState(text);
@@ -837,26 +862,25 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
if (derivedText !== text.toUpperCase()) {
setDerivedText(text.toUpperCase());
}
- return ;
+ return React.createElement(Text, {
+ text: derivedText,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
- await act(() => root.render());
-
+ await act(() => root.render(React.createElement(App, null)));
assertLog(['INITIAL']);
expect(container.textContent).toEqual('INITIAL');
-
await act(() => {
store.set('Updated');
});
assertLog(['UPDATED']);
expect(container.textContent).toEqual('UPDATED');
});
-
it('compares selection to rendered selection even if selector changes', async () => {
- const store = createExternalStore({items: ['A', 'B']});
-
+ const store = createExternalStore({
+ items: ['A', 'B'],
+ });
const shallowEqualArray = (a, b) => {
if (a.length !== b.length) {
return false;
@@ -868,19 +892,24 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
}
return true;
};
-
const List = React.memo(({items}) => {
- return (
-
- {items.map(text => (
- -
-
-
- ))}
-
+ return React.createElement(
+ 'ul',
+ null,
+ items.map(text =>
+ React.createElement(
+ 'li',
+ {
+ key: text,
+ },
+ React.createElement(Text, {
+ key: text,
+ text: text,
+ }),
+ ),
+ ),
);
});
-
function App({step}) {
const inlineSelector = state => {
Scheduler.log('Inline selector');
@@ -893,28 +922,37 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
inlineSelector,
shallowEqualArray,
);
- return (
- <>
-
-
- >
+ return React.createElement(
+ React.Fragment,
+ null,
+ React.createElement(List, {
+ items: items,
+ }),
+ React.createElement(Text, {
+ text: 'Sibling: ' + step,
+ }),
);
}
-
const container = document.createElement('div');
const root = createRoot(container);
await act(() => {
- root.render();
+ root.render(
+ React.createElement(App, {
+ step: 0,
+ }),
+ );
});
assertLog(['Inline selector', 'A', 'B', 'C', 'Sibling: 0']);
-
await act(() => {
- root.render();
+ root.render(
+ React.createElement(App, {
+ step: 1,
+ }),
+ );
});
assertLog([
// We had to call the selector again because it's not memoized
'Inline selector',
-
// But because the result was the same (according to isEqual) we can
// bail out of rendering the memoized list. These are skipped:
// 'A',
@@ -924,33 +962,38 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
'Sibling: 1',
]);
});
-
describe('selector and isEqual error handling in extra', () => {
let ErrorBoundary;
beforeEach(() => {
ErrorBoundary = class extends React.Component {
- state = {error: null};
+ state = {
+ error: null,
+ };
static getDerivedStateFromError(error) {
- return {error};
+ return {
+ error,
+ };
}
render() {
if (this.state.error) {
- return ;
+ return React.createElement(Text, {
+ text: this.state.error.message,
+ });
}
return this.props.children;
}
};
});
-
it('selector can throw on update', async () => {
- const store = createExternalStore({a: 'a'});
+ const store = createExternalStore({
+ a: 'a',
+ });
const selector = state => {
if (typeof state.a !== 'string') {
throw new TypeError('Malformed state');
}
return state.a.toUpperCase();
};
-
function App() {
const a = useSyncExternalStoreWithSelector(
store.subscribe,
@@ -958,22 +1001,23 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
null,
selector,
);
- return ;
+ return React.createElement(Text, {
+ text: a,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
await act(() =>
root.render(
-
-
- ,
+ React.createElement(
+ ErrorBoundary,
+ null,
+ React.createElement(App, null),
+ ),
),
);
-
assertLog(['A']);
expect(container.textContent).toEqual('A');
-
if (__DEV__ && gate(flags => flags.enableUseSyncExternalStoreShim)) {
// In 17, the error is re-thrown in DEV.
await expect(async () => {
@@ -986,12 +1030,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({});
});
}
-
expect(container.textContent).toEqual('Malformed state');
});
-
it('isEqual can throw on update', async () => {
- const store = createExternalStore({a: 'A'});
+ const store = createExternalStore({
+ a: 'A',
+ });
const selector = state => state.a;
const isEqual = (left, right) => {
if (typeof left.a !== 'string' || typeof right.a !== 'string') {
@@ -999,7 +1043,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
}
return left.a.trim() === right.a.trim();
};
-
function App() {
const a = useSyncExternalStoreWithSelector(
store.subscribe,
@@ -1008,22 +1051,23 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
selector,
isEqual,
);
- return ;
+ return React.createElement(Text, {
+ text: a,
+ });
}
-
const container = document.createElement('div');
const root = createRoot(container);
await act(() =>
root.render(
-
-
- ,
+ React.createElement(
+ ErrorBoundary,
+ null,
+ React.createElement(App, null),
+ ),
),
);
-
assertLog(['A']);
expect(container.textContent).toEqual('A');
-
if (__DEV__ && gate(flags => flags.enableUseSyncExternalStoreShim)) {
// In 17, the error is re-thrown in DEV.
await expect(async () => {
@@ -1036,7 +1080,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
store.set({});
});
}
-
expect(container.textContent).toEqual('Malformed state');
});
});
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 1fef8eb97413f..858c0519854b7 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -509,5 +509,6 @@
"521": "flushSyncWork should not be called from builds that support legacy mode. This is a bug in React.",
"522": "Invalid form element. requestFormReset must be passed a form that was rendered by React.",
"523": "The render was aborted due to being postponed.",
- "524": "Values cannot be passed to next() of AsyncIterables passed to Client Components."
+ "524": "Values cannot be passed to next() of AsyncIterables passed to Client Components.",
+ "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."
}