Skip to content

Commit 444db72

Browse files
authored
fix(react-bridge): Hoisting BridgeWrapper component to prevent unnecessary component recreation. (#4202)
1 parent d729167 commit 444db72

File tree

5 files changed

+266
-38
lines changed

5 files changed

+266
-38
lines changed

.changeset/eight-mirrors-drum.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'remote6': patch
3+
'@module-federation/bridge-react': patch
4+
---
5+
6+
fix(bridge-react): hoist BridgeWrapper to prevent component recreation

apps/router-demo/router-host-2000/cypress/e2e/remote6.cy.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,172 @@ describe('router-remote6-2006 in host', () => {
112112
cy.go('forward');
113113
cy.verifyContent('Remote6 about page');
114114
});
115+
116+
it('should increment outer counter without affecting inner counter', () => {
117+
cy.clickMenuItem('Remote6-ReactRouteV7');
118+
119+
// Navigate to detail page first
120+
cy.get('.self-remote6-detail-link').should('exist').click();
121+
cy.verifyContent('Remote6 detail page');
122+
123+
// Verify initial state
124+
cy.verifyContent('Outer Counter: 0');
125+
cy.get('[data-testid="remote6-inner-counter"]').should(
126+
'contain',
127+
'Inner Counter: 0',
128+
);
129+
130+
// Click outer increment button multiple times
131+
cy.get('[data-testid="remote6-outer-increment"]').click();
132+
cy.verifyContent('Outer Counter: 1');
133+
134+
cy.get('[data-testid="remote6-outer-increment"]').click();
135+
cy.verifyContent('Outer Counter: 2');
136+
137+
cy.get('[data-testid="remote6-outer-increment"]').click();
138+
cy.verifyContent('Outer Counter: 3');
139+
140+
// Inner counter should still be 0 (not affected by outer counter changes)
141+
cy.get('[data-testid="remote6-inner-counter"]').should(
142+
'contain',
143+
'Inner Counter: 0',
144+
);
145+
146+
// Increment outer counter again
147+
cy.get('[data-testid="remote6-outer-increment"]').click();
148+
cy.verifyContent('Outer Counter: 4');
149+
150+
// Inner counter should still be 0 (not affected)
151+
cy.get('[data-testid="remote6-inner-counter"]').should(
152+
'contain',
153+
'Inner Counter: 0',
154+
);
155+
});
156+
157+
it('should increment inner counter without affecting outer counter', () => {
158+
cy.clickMenuItem('Remote6-ReactRouteV7');
159+
160+
// Navigate to detail page first
161+
cy.get('.self-remote6-detail-link').should('exist').click();
162+
cy.verifyContent('Remote6 detail page');
163+
164+
// Verify initial state
165+
cy.verifyContent('Outer Counter: 0');
166+
cy.get('[data-testid="remote6-inner-counter"]').should(
167+
'contain',
168+
'Inner Counter: 0',
169+
);
170+
171+
// Click inner increment button
172+
cy.get('[data-testid="remote6-inner-increment"]').click();
173+
cy.get('[data-testid="remote6-inner-counter"]').should(
174+
'contain',
175+
'Inner Counter: 1',
176+
);
177+
178+
// Outer counter should still be 0 (not affected)
179+
cy.verifyContent('Outer Counter: 0');
180+
181+
// Increment inner counter more times
182+
cy.get('[data-testid="remote6-inner-increment"]').click();
183+
cy.get('[data-testid="remote6-inner-increment"]').click();
184+
cy.get('[data-testid="remote6-inner-counter"]').should(
185+
'contain',
186+
'Inner Counter: 3',
187+
);
188+
189+
// Outer counter should still be 0
190+
cy.verifyContent('Outer Counter: 0');
191+
192+
// Now increment outer counter
193+
cy.get('[data-testid="remote6-outer-increment"]').click();
194+
cy.verifyContent('Outer Counter: 1');
195+
196+
// Inner counter should be preserved at 3 (not reset - this proves BridgeWrapper is not recreated)
197+
cy.get('[data-testid="remote6-inner-counter"]').should(
198+
'contain',
199+
'Inner Counter: 3',
200+
);
201+
202+
// Increment outer counter more times
203+
cy.get('[data-testid="remote6-outer-increment"]').click();
204+
cy.get('[data-testid="remote6-outer-increment"]').click();
205+
cy.verifyContent('Outer Counter: 3');
206+
207+
// Inner counter should still be preserved at 3
208+
cy.get('[data-testid="remote6-inner-counter"]').should(
209+
'contain',
210+
'Inner Counter: 3',
211+
);
212+
});
213+
214+
it('should handle alternating clicks between outer and inner counters', () => {
215+
cy.clickMenuItem('Remote6-ReactRouteV7');
216+
217+
// Navigate to detail page first
218+
cy.get('.self-remote6-detail-link').should('exist').click();
219+
cy.verifyContent('Remote6 detail page');
220+
221+
// Verify initial state
222+
cy.verifyContent('Outer Counter: 0');
223+
cy.get('[data-testid="remote6-inner-counter"]').should(
224+
'contain',
225+
'Inner Counter: 0',
226+
);
227+
228+
// Click outer increment
229+
cy.get('[data-testid="remote6-outer-increment"]').click();
230+
cy.verifyContent('Outer Counter: 1');
231+
cy.get('[data-testid="remote6-inner-counter"]').should(
232+
'contain',
233+
'Inner Counter: 0',
234+
);
235+
236+
// Click inner increment
237+
cy.get('[data-testid="remote6-inner-increment"]').click();
238+
cy.verifyContent('Outer Counter: 1');
239+
cy.get('[data-testid="remote6-inner-counter"]').should(
240+
'contain',
241+
'Inner Counter: 1',
242+
);
243+
244+
// Click outer increment again
245+
cy.get('[data-testid="remote6-outer-increment"]').click();
246+
cy.verifyContent('Outer Counter: 2');
247+
cy.get('[data-testid="remote6-inner-counter"]').should(
248+
'contain',
249+
'Inner Counter: 1',
250+
);
251+
252+
// Click inner increment again
253+
cy.get('[data-testid="remote6-inner-increment"]').click();
254+
cy.verifyContent('Outer Counter: 2');
255+
cy.get('[data-testid="remote6-inner-counter"]').should(
256+
'contain',
257+
'Inner Counter: 2',
258+
);
259+
260+
// Click outer increment twice
261+
cy.get('[data-testid="remote6-outer-increment"]').click();
262+
cy.get('[data-testid="remote6-outer-increment"]').click();
263+
cy.verifyContent('Outer Counter: 4');
264+
cy.get('[data-testid="remote6-inner-counter"]').should(
265+
'contain',
266+
'Inner Counter: 2',
267+
);
268+
269+
// Click inner increment three times
270+
cy.get('[data-testid="remote6-inner-increment"]').click();
271+
cy.get('[data-testid="remote6-inner-increment"]').click();
272+
cy.get('[data-testid="remote6-inner-increment"]').click();
273+
cy.verifyContent('Outer Counter: 4');
274+
cy.get('[data-testid="remote6-inner-counter"]').should(
275+
'contain',
276+
'Inner Counter: 5',
277+
);
278+
279+
// Final verification: both counters maintain their independent state
280+
// This proves BridgeWrapper is not recreated and both states are preserved
281+
});
115282
});
116283
});

apps/router-demo/router-host-2000/src/App.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, {
33
useEffect,
44
ForwardRefExoticComponent,
55
Suspense,
6+
useState,
67
} from 'react';
78
import { Route, Routes, useLocation } from 'react-router-dom';
89
import {
@@ -284,16 +285,29 @@ const App = () => {
284285
/>
285286
<Route
286287
path="/remote6/*"
287-
Component={() => (
288-
<Remote6App
289-
rootOptions={{
290-
identifierPrefix: 'remote6-instance-',
291-
onRecoverableError: (error: Error) => {
292-
console.error('[Host] Remote6 recoverable error:', error);
293-
},
294-
}}
295-
/>
296-
)}
288+
Component={() => {
289+
const [counter, setCounter] = useState(0);
290+
291+
return (
292+
<>
293+
<button
294+
data-testid="remote6-outer-increment"
295+
onClick={() => setCounter(counter + 1)}
296+
>
297+
Increment
298+
</button>
299+
<Remote6App
300+
outerCounter={counter}
301+
rootOptions={{
302+
identifierPrefix: 'remote6-instance-',
303+
onRecoverableError: (error: Error) => {
304+
console.error('[Host] Remote6 recoverable error:', error);
305+
},
306+
}}
307+
/>
308+
</>
309+
);
310+
}}
297311
/>
298312
</Routes>
299313
</div>

apps/router-demo/router-remote6-2006/src/App.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from 'react-router';
99
import './App.css';
1010
import styled from '@emotion/styled';
11+
import { useState } from 'react';
1112

1213
const HomeDiv = styled.div`
1314
color: purple;
@@ -48,16 +49,28 @@ function Home() {
4849
}
4950

5051
function Detail() {
52+
const [counter, setCounter] = useState(0);
5153
return (
5254
<>
5355
<h2>Remote6 detail page</h2>
5456
<div>hello remote6 detail page with React Router v7</div>
5557
<div>🚀 Enhanced routing with better performance and DX</div>
56-
<Image
57-
width={200}
58-
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
59-
alt="Sample image"
60-
/>
58+
<div style={{ fontSize: 40 }} data-testid="remote6-inner-counter">
59+
Inner Counter: {counter}
60+
</div>
61+
<button
62+
data-testid="remote6-inner-increment"
63+
onClick={() => setCounter(counter + 1)}
64+
>
65+
Increment
66+
</button>
67+
<div>
68+
<Image
69+
width={200}
70+
src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
71+
alt="Sample image"
72+
/>
73+
</div>
6174
</>
6275
);
6376
}
@@ -171,7 +184,11 @@ const router = createBrowserRouter([
171184
},
172185
]);
173186

174-
const App = (info?: { basename?: string; initialEntries?: Array<string> }) => {
187+
const App = (info?: {
188+
outerCounter: number;
189+
basename?: string;
190+
initialEntries?: Array<string>;
191+
}) => {
175192
// React Router v7 supports more advanced routing features
176193
// For now, we'll use the basic router configuration
177194
// In a real app, you might want to handle basename and initialEntries
@@ -180,6 +197,7 @@ const App = (info?: { basename?: string; initialEntries?: Array<string> }) => {
180197

181198
return (
182199
<div className="remote6-app">
200+
<div style={{ fontSize: 40 }}>Outer Counter: {info?.outerCounter}</div>
183201
<RouterProvider router={router} />
184202
</div>
185203
);

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

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,38 @@ export function createBaseBridgeComponent<T>({
4343
);
4444
};
4545

46+
const DefaultFallback = ({ error }: FallbackProps) => (
47+
<div role="alert">
48+
<p>Something went wrong:</p>
49+
<pre style={{ color: 'red' }}>{error.message}</pre>
50+
</div>
51+
);
52+
53+
const BridgeWrapper = ({
54+
basename,
55+
moduleName,
56+
memoryRoute,
57+
propsInfo,
58+
fallback,
59+
}: {
60+
basename?: string;
61+
moduleName?: string;
62+
memoryRoute?: any;
63+
propsInfo: T;
64+
fallback?: React.ComponentType<FallbackProps>;
65+
}) => (
66+
<ErrorBoundary FallbackComponent={fallback || DefaultFallback}>
67+
<RawComponent
68+
appInfo={{
69+
moduleName,
70+
basename,
71+
memoryRoute,
72+
}}
73+
propsInfo={propsInfo}
74+
/>
75+
</ErrorBoundary>
76+
);
77+
4678
return {
4779
async render(info: RenderParams) {
4880
LoggerInstance.debug(`createBridgeComponent render Info`, info);
@@ -64,29 +96,20 @@ export function createBaseBridgeComponent<T>({
6496
const beforeBridgeRenderRes =
6597
instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(info) || {};
6698

67-
const BridgeWrapper = ({ basename }: { basename?: string }) => (
68-
<ErrorBoundary
69-
FallbackComponent={fallback as React.ComponentType<FallbackProps>}
70-
>
71-
<RawComponent
72-
appInfo={{
73-
moduleName,
74-
basename,
75-
memoryRoute,
76-
}}
77-
propsInfo={
78-
{
79-
...propsInfo,
80-
basename,
81-
...(beforeBridgeRenderRes as any)?.extraProps,
82-
} as T
83-
}
84-
/>
85-
</ErrorBoundary>
86-
);
87-
8899
const rootComponentWithErrorBoundary = (
89-
<BridgeWrapper basename={basename} />
100+
<BridgeWrapper
101+
basename={basename}
102+
moduleName={moduleName}
103+
memoryRoute={memoryRoute}
104+
fallback={fallback as React.ComponentType<FallbackProps>}
105+
propsInfo={
106+
{
107+
...propsInfo,
108+
basename,
109+
...(beforeBridgeRenderRes as any)?.extraProps,
110+
} as T
111+
}
112+
/>
90113
);
91114

92115
if (bridgeInfo.render) {

0 commit comments

Comments
 (0)