Skip to content

Commit

Permalink
Throw if React and React DOM versions don't match
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed May 23, 2024
1 parent ee5c194 commit 8ad45be
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 1 deletion.
3 changes: 3 additions & 0 deletions packages/react-dom/src/client/ReactDOMClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/client/ReactDOMClientFB.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerBun.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzStaticNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`,
);
}
}
127 changes: 127 additions & 0 deletions packages/react/src/__tests__/ReactMismatchedVersions-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* 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}`,
);
});

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}`,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
: 'react-dom-17/umd/react-dom.production.min.js',
),
);
jest.mock('react-dom/client', () =>
jest.requireActual(
__DEV__
? 'react-dom-17/umd/react-dom.development.js'
: 'react-dom-17/umd/react-dom.production.min.js',
),
);
// Because React 17 prints extra logs we need to ignore them.
originalError = console.error;
console.error = jest.fn();
Expand Down
3 changes: 2 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

0 comments on commit 8ad45be

Please sign in to comment.