Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw if React and React DOM versions don't match #29236

Merged
merged 2 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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',
);
}
}
14 changes: 14 additions & 0 deletions packages/react-native-renderer/src/ReactNativeRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ import {disableLegacyMode} from 'shared/ReactFeatureFlags';
// Module provided by RN:
import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';

import reactNativePackageVersion from 'shared/ReactVersion';
import * as IsomorphicReactPackage from 'react';

const isomorphicReactPackageVersion = IsomorphicReactPackage.version;
if (isomorphicReactPackageVersion !== reactNativePackageVersion) {
throw new Error(
'Incompatible React versions: The "react" and "react-native-renderer" packages must ' +
'have the exact same version. Instead got:\n' +
` - react: ${isomorphicReactPackageVersion}\n` +
` - react-native-renderer: ${reactNativePackageVersion}\n` +
'Learn more: https://react.dev/warnings/version-mismatch',
);
}

if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
throw new Error(
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
Expand Down
143 changes: 143 additions & 0 deletions packages/react/src/__tests__/ReactMismatchedVersions-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* 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;

let React;
let actualReactVersion;

beforeEach(() => {
jest.resetModules();
jest.mock('react', () => {
const actualReact = jest.requireActual('react');
return {
...actualReact,
version: '18.0.0-whoa-this-aint-the-right-react',
__actualVersion: actualReact.version,
};
});
React = require('react');
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}`,
);
});

// @gate source
test('importing "react-native-renderer" throws if version does not match React version', async () => {
expect(() => require('react-native-renderer')).toThrow(
'Incompatible React versions: The "react" and "react-native-renderer" packages ' +
'must have the exact same version. Instead got:\n' +
' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
` - react-native-renderer: ${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"
}