diff --git a/packages/react-dom/src/client/ReactDOMClient.js b/packages/react-dom/src/client/ReactDOMClient.js index 036fd846a47ba..3837f322a53ca 100644 --- a/packages/react-dom/src/client/ReactDOMClient.js +++ b/packages/react-dom/src/client/ReactDOMClient.js @@ -19,6 +19,9 @@ import ReactVersion from 'shared/ReactVersion'; import {getClosestInstanceFromNode} from 'react-dom-bindings/src/client/ReactDOMComponentTree'; import Internals from 'shared/ReactDOMSharedInternals'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + if (__DEV__) { if ( typeof Map !== 'function' || diff --git a/packages/react-dom/src/client/ReactDOMClientFB.js b/packages/react-dom/src/client/ReactDOMClientFB.js index 1f918e3cf68c0..a3985860728e6 100644 --- a/packages/react-dom/src/client/ReactDOMClientFB.js +++ b/packages/react-dom/src/client/ReactDOMClientFB.js @@ -25,6 +25,9 @@ import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal import {canUseDOM} from 'shared/ExecutionEnvironment'; import ReactVersion from 'shared/ReactVersion'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + import { getClosestInstanceFromNode, getInstanceFromNode, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 14c4a597922ee..8879a511d3fa0 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -37,6 +37,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + type Options = { identifierPrefix?: string, namespaceURI?: string, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js index 4cceb66e7cdf7..750c3133c4518 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBun.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -31,6 +31,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + type Options = { identifierPrefix?: string, namespaceURI?: string, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js index 14c4a597922ee..8879a511d3fa0 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js @@ -37,6 +37,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + type Options = { identifierPrefix?: string, namespaceURI?: string, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 049849a664559..f0c9d75fc7107 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -41,6 +41,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + function createDrainHandler(destination: Destination, request: Request) { return () => startFlowing(request, destination); } diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index cbc5cd4044361..f5d6a45a18c8c 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -36,6 +36,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + type Options = { identifierPrefix?: string, namespaceURI?: string, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index e1fc514b7da5b..1a2eb1e599afe 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -36,6 +36,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + type Options = { identifierPrefix?: string, namespaceURI?: string, diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index 3c3c4116a5e54..fc25aa75c190a 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -37,6 +37,9 @@ import { createRootFormatContext, } from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion'; +ensureCorrectIsomorphicReactVersion(); + type Options = { identifierPrefix?: string, namespaceURI?: string, diff --git a/packages/react-dom/src/shared/ensureCorrectIsomorphicReactVersion.js b/packages/react-dom/src/shared/ensureCorrectIsomorphicReactVersion.js new file mode 100644 index 0000000000000..c5ebb810ff30a --- /dev/null +++ b/packages/react-dom/src/shared/ensureCorrectIsomorphicReactVersion.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import reactDOMPackageVersion from 'shared/ReactVersion'; +import * as IsomorphicReactPackage from 'react'; + +export function ensureCorrectIsomorphicReactVersion() { + const isomorphicReactPackageVersion = IsomorphicReactPackage.version; + if (isomorphicReactPackageVersion !== reactDOMPackageVersion) { + // TODO: Do we have a canoncical documentation page for this? + throw new Error( + 'Incompatible React versions: The "react" and "react-dom" packages must ' + + 'have the exact same version. Instead got:\n' + + ` - react: ${isomorphicReactPackageVersion}\n` + + ` - react-dom: ${reactDOMPackageVersion}`, + ); + } +} diff --git a/packages/react/src/__tests__/ReactMismatchedVersions-test.js b/packages/react/src/__tests__/ReactMismatchedVersions-test.js new file mode 100644 index 0000000000000..4a20f490e410f --- /dev/null +++ b/packages/react/src/__tests__/ReactMismatchedVersions-test.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactMismatchedVersions-test', () => { + // Polyfills for test environment + global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; + global.TextEncoder = require('util').TextEncoder; + + jest.mock('react', () => { + const actualReact = jest.requireActual('react'); + return { + ...actualReact, + version: '18.0.0-whoa-this-aint-the-right-react', + __actualVersion: actualReact.version, + }; + }); + const React = require('react'); + const actualReactVersion = React.__actualVersion; + + test('importing "react-dom/client" throws if version does not match React version', async () => { + expect(() => require('react-dom/client')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + // When running in source mode, we lazily require the implementation to + // simulate the static config dependency injection we do at build time. So it + // only errors once you call something and trigger the require. Running the + // test in build mode is sufficient. + // @gate !source + test('importing "react-dom/server" throws if version does not match React version', async () => { + expect(() => require('react-dom/server')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + // @gate !source + test('importing "react-dom/server.node" throws if version does not match React version', async () => { + expect(() => require('react-dom/server.node')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + // @gate !source + test('importing "react-dom/server.browser" throws if version does not match React version', async () => { + expect(() => require('react-dom/server.browser')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + // @gate !source + test('importing "react-dom/server.bun" throws if version does not match React version', async () => { + expect(() => require('react-dom/server.bun')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + // @gate !source + test('importing "react-dom/server.edge" throws if version does not match React version', async () => { + expect(() => require('react-dom/server.edge')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + // @gate !source + test('importing "react-dom/static" throws if version does not match React version', async () => { + expect(() => require('react-dom/static')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + test('importing "react-dom/static.node" throws if version does not match React version', async () => { + expect(() => require('react-dom/static.node')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + test('importing "react-dom/static.browser" throws if version does not match React version', async () => { + expect(() => require('react-dom/static.browser')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); + + test('importing "react-dom/static.edge" throws if version does not match React version', async () => { + expect(() => require('react-dom/static.edge')).toThrow( + 'Incompatible React versions: The "react" and "react-dom" packages ' + + 'must have the exact same version. Instead got:\n' + + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' + + ` - react-dom: ${actualReactVersion}`, + ); + }); +}); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index f0e73bd6fb4f3..eba4a6715d3fd 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -511,5 +511,6 @@ "523": "The render was aborted due to being postponed.", "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.", - "526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server." + "526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server.", + "527": "Incompatible React versions: The \"react\" and \"react-dom\" packages must have the exact same version. Instead got:\n - react: %s\n - react-dom: %s" }