Skip to content

Commit 646314d

Browse files
authored
fix(bridge-react): the bridge-react package was failing when used with React 16/17 due to missing react-dom/client module (#3500)
1 parent 2a245df commit 646314d

File tree

5 files changed

+884
-59
lines changed

5 files changed

+884
-59
lines changed

.changeset/calm-tigers-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/bridge-react': patch
3+
---
4+
5+
fix: the bridge-react package was failing when used with React 16 due to missing react-dom/client module

apps/router-demo/router-remote1-2001/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
"@module-federation/bridge-react": "workspace:*",
1212
"@module-federation/rsbuild-plugin": "workspace:*",
1313
"antd": "^5.16.2",
14-
"react": "^18.3.1",
15-
"react-dom": "^18.3.1",
14+
"react": "^17.0.2",
15+
"react-dom": "^17.0.2",
1616
"react-router-dom": "^5.3.4",
1717
"react-shadow": "^20.4.0"
1818
},
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import ReactDOM from 'react-dom';
2+
3+
interface CreateRootOptions {
4+
identifierPrefix?: string;
5+
onRecoverableError?: (error: unknown) => void;
6+
transitionCallbacks?: unknown;
7+
}
8+
9+
interface Root {
10+
render(children: React.ReactNode): void;
11+
unmount(): void;
12+
}
13+
14+
const isReact18 = ReactDOM.version.startsWith('18');
15+
16+
/**
17+
* Creates a root for a container element compatible with both React 16 and 18
18+
*/
19+
export function createRoot(
20+
container: Element | DocumentFragment,
21+
options?: CreateRootOptions,
22+
): Root {
23+
if (isReact18) {
24+
// For React 18, use the new createRoot API
25+
// @ts-ignore - Types will be available in React 18
26+
return (ReactDOM as any).createRoot(container, options);
27+
}
28+
29+
// For React 16/17, simulate the new root API using render/unmountComponentAtNode
30+
return {
31+
render(children: React.ReactNode) {
32+
// @ts-ignore - React 17's render method is deprecated but still functional
33+
ReactDOM.render(children, container);
34+
},
35+
unmount() {
36+
ReactDOM.unmountComponentAtNode(container);
37+
},
38+
};
39+
}
40+
41+
/**
42+
* Hydrates a container compatible with both React 16 and 18
43+
*/
44+
export function hydrateRoot(
45+
container: Element | DocumentFragment,
46+
initialChildren: React.ReactNode,
47+
options?: CreateRootOptions,
48+
): Root {
49+
if (isReact18) {
50+
// For React 18, use the new hydrateRoot API
51+
// @ts-ignore - Types will be available in React 18
52+
return (ReactDOM as any).hydrateRoot(container, initialChildren, options);
53+
}
54+
55+
// For React 16/17, simulate the new root API using hydrate
56+
return {
57+
render(children: React.ReactNode) {
58+
// @ts-ignore - React 17's hydrate method is deprecated but still functional
59+
ReactDOM.hydrate(children, container);
60+
},
61+
unmount() {
62+
ReactDOM.unmountComponentAtNode(container);
63+
},
64+
};
65+
}

packages/bridge/bridge-react/src/provider.tsx

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { useLayoutEffect, useRef, useState } from 'react';
21
import * as React from 'react';
32
import ReactDOM from 'react-dom';
4-
import ReactDOMClient from 'react-dom/client';
53
import type {
64
ProviderParams,
75
RenderFnParams,
86
} from '@module-federation/bridge-shared';
97
import { ErrorBoundary } from 'react-error-boundary';
108
import { RouterContext } from './context';
11-
import { LoggerInstance, atLeastReact18 } from './utils';
9+
import { LoggerInstance } from './utils';
1210
import { federationRuntime } from './plugin';
11+
import { createRoot } from './compat';
1312

1413
type RenderParams = RenderFnParams & {
1514
[key: string]: unknown;
@@ -18,7 +17,7 @@ type DestroyParams = {
1817
moduleName: string;
1918
dom: HTMLElement;
2019
};
21-
type RootType = HTMLElement | ReactDOMClient.Root;
20+
type RootType = HTMLElement | ReturnType<typeof createRoot>;
2221

2322
export type ProviderFnParams<T> = {
2423
rootComponent: React.ComponentType<T>;
@@ -67,7 +66,6 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
6766
instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(info) || {};
6867

6968
const rootComponentWithErrorBoundary = (
70-
// set ErrorBoundary for RawComponent rendering error, usually caused by user app rendering error
7169
<ErrorBoundary FallbackComponent={fallback}>
7270
<RawComponent
7371
appInfo={{
@@ -81,53 +79,34 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
8179
/>
8280
</ErrorBoundary>
8381
);
84-
// call render function
85-
if (atLeastReact18(React)) {
86-
if (bridgeInfo?.render) {
87-
// in case bridgeInfo?.render is an async function, resolve this to promise
88-
Promise.resolve(
89-
bridgeInfo?.render(rootComponentWithErrorBoundary, dom),
90-
).then((root: RootType) => rootMap.set(info.dom, root));
91-
} else {
92-
const root: RootType = ReactDOMClient.createRoot(info.dom);
93-
root.render(rootComponentWithErrorBoundary);
94-
rootMap.set(info.dom, root);
95-
}
82+
83+
if (bridgeInfo?.render) {
84+
// in case bridgeInfo?.render is an async function, resolve this to promise
85+
Promise.resolve(
86+
bridgeInfo?.render(rootComponentWithErrorBoundary, dom),
87+
).then((root: RootType) => rootMap.set(info.dom, root));
9688
} else {
97-
// react 17 render
98-
const renderFn = bridgeInfo?.render || ReactDOM.render;
99-
renderFn?.(rootComponentWithErrorBoundary, info.dom);
89+
const root = createRoot(info.dom);
90+
root.render(rootComponentWithErrorBoundary);
91+
rootMap.set(info.dom, root);
10092
}
93+
10194
instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(info) || {};
10295
},
10396

104-
async destroy(info: DestroyParams) {
105-
LoggerInstance.debug(`createBridgeComponent destroy Info`, {
106-
dom: info.dom,
107-
});
108-
instance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit(info);
109-
110-
// call destroy function
111-
if (atLeastReact18(React)) {
112-
const root = rootMap.get(info.dom);
113-
(root as ReactDOMClient.Root)?.unmount();
97+
destroy(info: DestroyParams) {
98+
LoggerInstance.debug(`createBridgeComponent destroy Info`, info);
99+
const root = rootMap.get(info.dom);
100+
if (root) {
101+
if ('unmount' in root) {
102+
root.unmount();
103+
} else {
104+
ReactDOM.unmountComponentAtNode(root as HTMLElement);
105+
}
114106
rootMap.delete(info.dom);
115-
} else {
116-
ReactDOM.unmountComponentAtNode(info.dom);
117107
}
118-
119-
instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit(info);
108+
instance?.bridgeHook?.lifecycle?.destroyBridge?.emit(info);
120109
},
121-
rawComponent: bridgeInfo.rootComponent,
122-
__BRIDGE_FN__: (_args: T) => {},
123110
};
124111
};
125112
}
126-
127-
export function ShadowRoot(info: { children: () => JSX.Element }) {
128-
const [root] = useState(null);
129-
const domRef = useRef(null);
130-
useLayoutEffect(() => {});
131-
132-
return <div ref={domRef}>{root && <info.children />}</div>;
133-
}

0 commit comments

Comments
 (0)