@samitouri / QOS-React / commits / 681a4aa810

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

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.

Andrew Clark committed May 28, 2024 at 14:06 UTC 681a4aa81022d4053f990d905d6453c73d2ee644
14 files changed +217 -1
packages/react-dom/src/client/ReactDOMClient.js
+3
@@ -19,6 +19,9 @@ import ReactVersion from 'shared/ReactVersion';
19 import {getClosestInstanceFromNode} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
20 import Internals from 'shared/ReactDOMSharedInternals';
21
22 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
23 +ensureCorrectIsomorphicReactVersion();
24 +
25 if (__DEV__) {
26 if (
27 typeof Map !== 'function' ||
packages/react-dom/src/client/ReactDOMClientFB.js
+3
@@ -25,6 +25,9 @@ import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal
25 import {canUseDOM} from 'shared/ExecutionEnvironment';
26 import ReactVersion from 'shared/ReactVersion';
27
28 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
29 +ensureCorrectIsomorphicReactVersion();
30 +
31 import {
32 getClosestInstanceFromNode,
33 getInstanceFromNode,
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+3
@@ -37,6 +37,9 @@ import {
37 createRootFormatContext,
38 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
39
40 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
41 +ensureCorrectIsomorphicReactVersion();
42 +
43 type Options = {
44 identifierPrefix?: string,
45 namespaceURI?: string,
packages/react-dom/src/server/ReactDOMFizzServerBun.js
+3
@@ -31,6 +31,9 @@ import {
31 createRootFormatContext,
32 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
33
34 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
35 +ensureCorrectIsomorphicReactVersion();
36 +
37 type Options = {
38 identifierPrefix?: string,
39 namespaceURI?: string,
packages/react-dom/src/server/ReactDOMFizzServerEdge.js
+3
@@ -37,6 +37,9 @@ import {
37 createRootFormatContext,
38 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
39
40 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
41 +ensureCorrectIsomorphicReactVersion();
42 +
43 type Options = {
44 identifierPrefix?: string,
45 namespaceURI?: string,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+3
@@ -41,6 +41,9 @@ import {
41 createRootFormatContext,
42 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
43
44 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
45 +ensureCorrectIsomorphicReactVersion();
46 +
47 function createDrainHandler(destination: Destination, request: Request) {
48 return () => startFlowing(request, destination);
49 }
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+3
@@ -36,6 +36,9 @@ import {
36 createRootFormatContext,
37 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
38
39 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
40 +ensureCorrectIsomorphicReactVersion();
41 +
42 type Options = {
43 identifierPrefix?: string,
44 namespaceURI?: string,
packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
+3
@@ -36,6 +36,9 @@ import {
36 createRootFormatContext,
37 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
38
39 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
40 +ensureCorrectIsomorphicReactVersion();
41 +
42 type Options = {
43 identifierPrefix?: string,
44 namespaceURI?: string,
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+3
@@ -37,6 +37,9 @@ import {
37 createRootFormatContext,
38 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
39
40 +import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
41 +ensureCorrectIsomorphicReactVersion();
42 +
43 type Options = {
44 identifierPrefix?: string,
45 namespaceURI?: string,
packages/react-dom/src/shared/ensureCorrectIsomorphicReactVersion.js new
+24
@@ -0,0 +1,24 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import reactDOMPackageVersion from 'shared/ReactVersion';
11 +import * as IsomorphicReactPackage from 'react';
12 +
13 +export function ensureCorrectIsomorphicReactVersion() {
14 + const isomorphicReactPackageVersion = IsomorphicReactPackage.version;
15 + if (isomorphicReactPackageVersion !== reactDOMPackageVersion) {
16 + throw new Error(
17 + 'Incompatible React versions: The "react" and "react-dom" packages must ' +
18 + 'have the exact same version. Instead got:\n' +
19 + ` - react: ${isomorphicReactPackageVersion}\n` +
20 + ` - react-dom: ${reactDOMPackageVersion}\n` +
21 + 'Learn more: https://react.dev/warnings/version-mismatch',
22 + );
23 + }
24 +}
packages/react-native-renderer/src/ReactNativeRenderer.js
+14
@@ -56,6 +56,20 @@ import {disableLegacyMode} from 'shared/ReactFeatureFlags';
56 // Module provided by RN:
57 import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
58
59 +import reactNativePackageVersion from 'shared/ReactVersion';
60 +import * as IsomorphicReactPackage from 'react';
61 +
62 +const isomorphicReactPackageVersion = IsomorphicReactPackage.version;
63 +if (isomorphicReactPackageVersion !== reactNativePackageVersion) {
64 + throw new Error(
65 + 'Incompatible React versions: The "react" and "react-native-renderer" packages must ' +
66 + 'have the exact same version. Instead got:\n' +
67 + ` - react: ${isomorphicReactPackageVersion}\n` +
68 + ` - react-native-renderer: ${reactNativePackageVersion}\n` +
69 + 'Learn more: https://react.dev/warnings/version-mismatch',
70 + );
71 +}
72 +
73 if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
74 throw new Error(
75 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
packages/react/src/__tests__/ReactMismatchedVersions-test.js new
+143
@@ -0,0 +1,143 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +describe('ReactMismatchedVersions-test', () => {
13 + // Polyfills for test environment
14 + global.ReadableStream =
15 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
16 + global.TextEncoder = require('util').TextEncoder;
17 +
18 + let React;
19 + let actualReactVersion;
20 +
21 + beforeEach(() => {
22 + jest.resetModules();
23 + jest.mock('react', () => {
24 + const actualReact = jest.requireActual('react');
25 + return {
26 + ...actualReact,
27 + version: '18.0.0-whoa-this-aint-the-right-react',
28 + __actualVersion: actualReact.version,
29 + };
30 + });
31 + React = require('react');
32 + actualReactVersion = React.__actualVersion;
33 + });
34 +
35 + test('importing "react-dom/client" throws if version does not match React version', async () => {
36 + expect(() => require('react-dom/client')).toThrow(
37 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
38 + 'must have the exact same version. Instead got:\n' +
39 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
40 + ` - react-dom: ${actualReactVersion}`,
41 + );
42 + });
43 +
44 + // When running in source mode, we lazily require the implementation to
45 + // simulate the static config dependency injection we do at build time. So it
46 + // only errors once you call something and trigger the require. Running the
47 + // test in build mode is sufficient.
48 + // @gate !source
49 + test('importing "react-dom/server" throws if version does not match React version', async () => {
50 + expect(() => require('react-dom/server')).toThrow(
51 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
52 + 'must have the exact same version. Instead got:\n' +
53 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
54 + ` - react-dom: ${actualReactVersion}`,
55 + );
56 + });
57 +
58 + // @gate !source
59 + test('importing "react-dom/server.node" throws if version does not match React version', async () => {
60 + expect(() => require('react-dom/server.node')).toThrow(
61 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
62 + 'must have the exact same version. Instead got:\n' +
63 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
64 + ` - react-dom: ${actualReactVersion}`,
65 + );
66 + });
67 +
68 + // @gate !source
69 + test('importing "react-dom/server.browser" throws if version does not match React version', async () => {
70 + expect(() => require('react-dom/server.browser')).toThrow(
71 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
72 + 'must have the exact same version. Instead got:\n' +
73 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
74 + ` - react-dom: ${actualReactVersion}`,
75 + );
76 + });
77 +
78 + // @gate !source
79 + test('importing "react-dom/server.bun" throws if version does not match React version', async () => {
80 + expect(() => require('react-dom/server.bun')).toThrow(
81 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
82 + 'must have the exact same version. Instead got:\n' +
83 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
84 + ` - react-dom: ${actualReactVersion}`,
85 + );
86 + });
87 +
88 + // @gate !source
89 + test('importing "react-dom/server.edge" throws if version does not match React version', async () => {
90 + expect(() => require('react-dom/server.edge')).toThrow(
91 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
92 + 'must have the exact same version. Instead got:\n' +
93 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
94 + ` - react-dom: ${actualReactVersion}`,
95 + );
96 + });
97 +
98 + test('importing "react-dom/static" throws if version does not match React version', async () => {
99 + expect(() => require('react-dom/static')).toThrow(
100 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
101 + 'must have the exact same version. Instead got:\n' +
102 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
103 + ` - react-dom: ${actualReactVersion}`,
104 + );
105 + });
106 +
107 + test('importing "react-dom/static.node" throws if version does not match React version', async () => {
108 + expect(() => require('react-dom/static.node')).toThrow(
109 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
110 + 'must have the exact same version. Instead got:\n' +
111 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
112 + ` - react-dom: ${actualReactVersion}`,
113 + );
114 + });
115 +
116 + test('importing "react-dom/static.browser" throws if version does not match React version', async () => {
117 + expect(() => require('react-dom/static.browser')).toThrow(
118 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
119 + 'must have the exact same version. Instead got:\n' +
120 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
121 + ` - react-dom: ${actualReactVersion}`,
122 + );
123 + });
124 +
125 + test('importing "react-dom/static.edge" throws if version does not match React version', async () => {
126 + expect(() => require('react-dom/static.edge')).toThrow(
127 + 'Incompatible React versions: The "react" and "react-dom" packages ' +
128 + 'must have the exact same version. Instead got:\n' +
129 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
130 + ` - react-dom: ${actualReactVersion}`,
131 + );
132 + });
133 +
134 + // @gate source
135 + test('importing "react-native-renderer" throws if version does not match React version', async () => {
136 + expect(() => require('react-native-renderer')).toThrow(
137 + 'Incompatible React versions: The "react" and "react-native-renderer" packages ' +
138 + 'must have the exact same version. Instead got:\n' +
139 + ' - react: 18.0.0-whoa-this-aint-the-right-react\n' +
140 + ` - react-native-renderer: ${actualReactVersion}`,
141 + );
142 + });
143 +});
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+7
@@ -43,6 +43,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
43 : 'react-dom-17/umd/react-dom.production.min.js',
44 ),
45 );
46 + jest.mock('react-dom/client', () =>
47 + jest.requireActual(
48 + __DEV__
49 + ? 'react-dom-17/umd/react-dom.development.js'
50 + : 'react-dom-17/umd/react-dom.production.min.js',
51 + ),
52 + );
53 // Because React 17 prints extra logs we need to ignore them.
54 originalError = console.error;
55 console.error = jest.fn();
scripts/error-codes/codes.json
+2 -1
@@ -511,5 +511,6 @@
511 "523": "The render was aborted due to being postponed.",
512 "524": "Values cannot be passed to next() of AsyncIterables passed to Client Components.",
513 "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.",
514 - "526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server."
514 + "526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server.",
515 + "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"
516 }