Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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.
  • Loading branch information
acdlite committed May 28, 2024
commit 8dd152f90e690f6f679ad3815d3e391a2e73b218
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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we compile to CJS this doesn't matter much but it might be nice to import {version} from "react" so that it doesn't pull in every export and disables dead export elimination.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been doing it this way based on this comment. Not sure if it still applies:

// This module only exists as an ESM wrapper around the external CommonJS
// Scheduler dependency. Notice that we're intentionally not using named imports
// because Rollup would use dynamic dispatch for CommonJS interop named imports.
// When we switch to ESM, we can delete this module.
import * as Scheduler from 'scheduler';


export function ensureCorrectIsomorphicReactVersion() {
const isomorphicReactPackageVersion = IsomorphicReactPackage.version;
if (isomorphicReactPackageVersion !== reactDOMPackageVersion) {
throw new Error(
'Incompatible React versions: The "react" and "react-dom" packages must ' +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a perfect world we'd also detect environment mismatches e.g. importing react-dom with the react-server condition while react is being imported without the react-server condition. Maybe we encode the environment in the version string e.g. 19.0.0-abc-123+react-server? Though that wouldn't tell you explcitly that the environment mismatched not the version.

Copy link
Collaborator Author

@acdlite acdlite May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add another internal field (or check for the presence of a server-only API, and vice versa), but I'm less concerned about that one since it's only the tip of the iceberg of what you have to do to configure a Server Components set-up correctly. For a similar reason I didn't add a check to React Native because nobody really imports the React Native renderer directly; it's configured by some sort of framework.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we actually add it for React Native too? Too often the RN sync are using the wrong versions together, which caused issues when landing breaking changes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like, RN installs the renderer, but the template still allows using your own version of react, and we should catch that. This will start erroring in the OSS build on land, but that needs to get fixed before the next RN npm release.

'have the exact same version. Instead got:\n' +
` - react: ${isomorphicReactPackageVersion}\n` +
` - react-dom: ${reactDOMPackageVersion}\n` +
'Learn more: https://react.dev/warnings/version-mismatch',
);
}
}
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\nLearn more: https://react.dev/warnings/version-mismatch"
}