From afb7de33119276cf2c2877375d1d7df44d319f41 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 23 May 2024 12:44:50 -0400 Subject: [PATCH] Throw if React and React DOM versions don't match Throw an error during module initialization if the version of the "react-dom" package does not match the version of "react". We used to be more relaxed about this, because the "react" package changed so infrequently. However, we now have many more features that rely on an internal protocol between the two packages, including Hooks, Float, and the compiler runtime. So it's important that both packages are versioned in lockstep. Before this change, a version mismatch would often result in a cryptic internal error with no indication of the root cause. Instead, we will now compare the versions during module initialization and immediately throw an error to catch mistakes as early as possible and provide a clear error message. --- .../react-dom/src/client/ReactDOMClient.js | 3 + .../src/server/ReactDOMFizzServerBrowser.js | 3 + .../src/server/ReactDOMFizzServerBun.js | 3 + .../src/server/ReactDOMFizzServerEdge.js | 3 + .../src/server/ReactDOMFizzServerNode.js | 3 + .../src/server/ReactDOMFizzStaticBrowser.js | 3 + .../src/server/ReactDOMFizzStaticEdge.js | 3 + .../src/server/ReactDOMFizzStaticNode.js | 3 + .../ensureCorrectIsomorphicReactVersion.js | 24 ++++ .../__tests__/ReactMismatchedVersions-test.js | 123 ++++++++++++++++++ scripts/error-codes/codes.json | 3 +- 11 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 packages/react-dom/src/shared/ensureCorrectIsomorphicReactVersion.js create mode 100644 packages/react/src/__tests__/ReactMismatchedVersions-test.js 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/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..421d58335c12d --- /dev/null +++ b/packages/react/src/__tests__/ReactMismatchedVersions-test.js @@ -0,0 +1,123 @@ +/** + * 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', () => { + 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" }