Skip to content

Commit 578aa43

Browse files
authored
fix(bridge-react): prevent destroy/render the remote component every time after the states changed (#3543)
1 parent 3081511 commit 578aa43

File tree

8 files changed

+121
-102
lines changed

8 files changed

+121
-102
lines changed

.changeset/dry-keys-taste.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': minor
3+
---
4+
5+
fix(bridge-react): prevent destroy/render the remote component every time after the states changed

apps/router-demo/router-host-2000/src/pages/Home.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { useState } from 'react';
12
import { Space, Table, Tag } from 'antd';
23
import type { TableProps } from 'antd';
34
import type React from 'react';
5+
import { init, loadRemote } from '@module-federation/enhanced/runtime';
6+
import { createRemoteComponent } from '@module-federation/bridge-react';
47

58
interface DataType {
69
key: string;
@@ -83,11 +86,23 @@ const data: DataType[] = [
8386
},
8487
];
8588

89+
const Remote1Button = createRemoteComponent<any, any>({
90+
loader: () => loadRemote('remote1/export-button'),
91+
// @ts-ignore
92+
fallback: null,
93+
loading: null,
94+
});
95+
8696
const Home: React.FC = () => {
97+
const [count, setCount] = useState(0);
8798
return (
8899
<>
89100
<h2>Router host Home page</h2>
90101
<Table columns={columns} dataSource={data} />
102+
<Remote1Button
103+
text={`Hit me! ${count}`}
104+
onClick={() => setCount((prevState: number) => prevState + 1)}
105+
/>
91106
</>
92107
);
93108
};

apps/router-demo/router-remote1-2001/rsbuild.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default defineConfig({
3939
exposes: {
4040
'./button': './src/button.tsx',
4141
'./export-app': './src/export-App.tsx',
42+
'./export-button': './src/export-Button.tsx',
4243
'./app': './src/App.tsx',
4344
},
4445
shared: {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export default function Button() {
2-
return <button>Provider button</button>;
1+
export default function Button(props: { text: string; onClick: () => void }) {
2+
return <button onClick={props.onClick}>{props.text}</button>;
33
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import Button from './button';
3+
import { createBridgeComponent } from '@module-federation/bridge-react';
4+
5+
const provider = createBridgeComponent({
6+
rootComponent: Button,
7+
});
8+
9+
export default provider;

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,13 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
8686
bridgeInfo?.render(rootComponentWithErrorBoundary, dom),
8787
).then((root: RootType) => rootMap.set(info.dom, root));
8888
} else {
89-
const root = createRoot(info.dom);
89+
let root = rootMap.get(info.dom);
90+
// do not call createRoot multiple times
91+
if (!root) {
92+
root = createRoot(info.dom);
93+
rootMap.set(info.dom, root);
94+
}
9095
root.render(rootComponentWithErrorBoundary);
91-
rootMap.set(info.dom, root);
9296
}
9397

9498
instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(info) || {};
@@ -105,7 +109,7 @@ export function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>) {
105109
}
106110
rootMap.delete(info.dom);
107111
}
108-
instance?.bridgeHook?.lifecycle?.destroyBridge?.emit(info);
112+
instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit(info);
109113
},
110114
};
111115
};

packages/bridge/bridge-react/src/remote/component.tsx

Lines changed: 81 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -40,112 +40,97 @@ const RemoteAppWrapper = forwardRef(function (
4040
props: RemoteAppParams & RenderFnParams,
4141
ref,
4242
) {
43-
const RemoteApp = () => {
44-
LoggerInstance.debug(`RemoteAppWrapper RemoteApp props >>>`, { props });
45-
const {
46-
moduleName,
47-
memoryRoute,
48-
basename,
49-
providerInfo,
50-
className,
51-
style,
52-
fallback,
53-
...resProps
54-
} = props;
55-
56-
const instance = federationRuntime.instance;
57-
const rootRef: React.MutableRefObject<HTMLDivElement | null> =
58-
ref && 'current' in ref
59-
? (ref as React.MutableRefObject<HTMLDivElement | null>)
60-
: useRef(null);
61-
62-
const renderDom: React.MutableRefObject<HTMLElement | null> = useRef(null);
63-
const providerInfoRef = useRef<any>(null);
43+
const {
44+
moduleName,
45+
memoryRoute,
46+
basename,
47+
providerInfo,
48+
className,
49+
style,
50+
fallback,
51+
...resProps
52+
} = props;
53+
54+
const instance = federationRuntime.instance;
55+
const rootRef: React.MutableRefObject<HTMLDivElement | null> =
56+
ref && 'current' in ref
57+
? (ref as React.MutableRefObject<HTMLDivElement | null>)
58+
: useRef(null);
59+
60+
const renderDom: React.MutableRefObject<HTMLElement | null> = useRef(null);
61+
const providerInfoRef = useRef<any>(null);
62+
const [initialized, setInitialized] = useState(false);
63+
64+
LoggerInstance.debug(`RemoteAppWrapper instance from props >>>`, instance);
65+
66+
// 初始化远程组件
67+
useEffect(() => {
68+
if (initialized) return;
69+
const providerReturn = providerInfo();
70+
providerInfoRef.current = providerReturn;
71+
setInitialized(true);
72+
73+
return () => {
74+
if (providerInfoRef.current?.destroy) {
75+
LoggerInstance.debug(
76+
`createRemoteComponent LazyComponent destroy >>>`,
77+
{ moduleName, basename, dom: renderDom.current },
78+
);
6479

65-
LoggerInstance.debug(`RemoteAppWrapper instance from props >>>`, instance);
80+
instance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit({
81+
moduleName,
82+
dom: renderDom.current,
83+
basename,
84+
memoryRoute,
85+
fallback,
86+
...resProps,
87+
});
6688

67-
useEffect(() => {
68-
const renderTimeout = setTimeout(() => {
69-
const providerReturn = providerInfo();
70-
providerInfoRef.current = providerReturn;
89+
providerInfoRef.current?.destroy({
90+
moduleName,
91+
dom: renderDom.current,
92+
});
7193

72-
let renderProps = {
94+
instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit({
7395
moduleName,
74-
dom: rootRef.current,
96+
dom: renderDom.current,
7597
basename,
7698
memoryRoute,
7799
fallback,
78100
...resProps,
79-
};
80-
renderDom.current = rootRef.current;
81-
LoggerInstance.debug(
82-
`createRemoteComponent LazyComponent render >>>`,
83-
renderProps,
84-
);
85-
86-
LoggerInstance.debug(
87-
`createRemoteComponent LazyComponent hostInstance >>>`,
88-
instance,
89-
);
90-
const beforeBridgeRenderRes =
91-
instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(
92-
renderProps,
93-
) || {};
94-
// @ts-ignore
95-
renderProps = { ...renderProps, ...beforeBridgeRenderRes.extraProps };
96-
providerReturn.render(renderProps);
97-
instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(renderProps);
98-
});
99-
100-
return () => {
101-
clearTimeout(renderTimeout);
102-
setTimeout(() => {
103-
if (providerInfoRef.current?.destroy) {
104-
LoggerInstance.debug(
105-
`createRemoteComponent LazyComponent destroy >>>`,
106-
{ moduleName, basename, dom: renderDom.current },
107-
);
108-
109-
instance?.bridgeHook?.lifecycle?.beforeBridgeDestroy?.emit({
110-
moduleName,
111-
dom: renderDom.current,
112-
basename,
113-
memoryRoute,
114-
fallback,
115-
...resProps,
116-
});
117-
118-
providerInfoRef.current?.destroy({
119-
moduleName,
120-
dom: renderDom.current,
121-
});
122-
123-
instance?.bridgeHook?.lifecycle?.afterBridgeDestroy?.emit({
124-
moduleName,
125-
dom: renderDom.current,
126-
basename,
127-
memoryRoute,
128-
fallback,
129-
...resProps,
130-
});
131-
}
132101
});
133-
};
134-
}, []);
135-
136-
// bridge-remote-root
137-
const rootComponentClassName = `${getRootDomDefaultClassName(moduleName)} ${props?.className}`;
138-
return (
139-
<div
140-
className={rootComponentClassName}
141-
style={props?.style}
142-
ref={rootRef}
143-
></div>
144-
);
145-
};
102+
}
103+
};
104+
}, [moduleName]);
146105

147-
(RemoteApp as any)['__APP_VERSION__'] = __APP_VERSION__;
148-
return <RemoteApp />;
106+
// trigger render after props updated
107+
useEffect(() => {
108+
if (!initialized || !providerInfoRef.current) return;
109+
110+
let renderProps = {
111+
moduleName,
112+
dom: rootRef.current,
113+
basename,
114+
memoryRoute,
115+
fallback,
116+
...resProps,
117+
};
118+
renderDom.current = rootRef.current;
119+
120+
const beforeBridgeRenderRes =
121+
instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(renderProps) ||
122+
{};
123+
// @ts-ignore
124+
renderProps = { ...renderProps, ...beforeBridgeRenderRes.extraProps };
125+
providerInfoRef.current.render(renderProps);
126+
instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(renderProps);
127+
}, [initialized, ...Object.values(props)]);
128+
129+
// bridge-remote-root
130+
const rootComponentClassName = `${getRootDomDefaultClassName(moduleName)} ${className || ''}`;
131+
return (
132+
<div className={rootComponentClassName} style={style} ref={rootRef}></div>
133+
);
149134
});
150135

151136
interface ExtraDataProps {

packages/bridge/bridge-react/src/remote/create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ export function createRemoteComponent<T, E extends keyof T>(
100100
: {}
101101
: {};
102102

103+
const LazyComponent = createLazyRemoteComponent(info);
103104
return forwardRef<HTMLDivElement, ProviderParams & RawComponentType>(
104105
(props, ref) => {
105-
const LazyComponent = createLazyRemoteComponent(info);
106106
return (
107107
<ErrorBoundary FallbackComponent={info.fallback}>
108108
<React.Suspense fallback={info.loading}>

0 commit comments

Comments
 (0)