-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
docs: enhance dynamic-remotes example documentation and modernization guide #4360
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
Changes from 5 commits
9563613
1cb6629
839f7a5
4d79e8e
02147ac
64bdb58
ef76b5c
813592f
0d35c77
6edea24
431d2ac
955268b
75a5cc7
f0ed529
ca671d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,39 +1,115 @@ | ||||||||||||||||||
import React, { useState, useEffect, Suspense } from 'react'; | ||||||||||||||||||
import { init, loadRemote } from '@module-federation/runtime'; | ||||||||||||||||||
|
||||||||||||||||||
class ErrorBoundary extends React.Component { | ||||||||||||||||||
constructor(props) { | ||||||||||||||||||
super(props); | ||||||||||||||||||
this.state = { hasError: false, error: null }; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
static getDerivedStateFromError(error) { | ||||||||||||||||||
return { hasError: true, error }; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
componentDidCatch(error, errorInfo) { | ||||||||||||||||||
console.error('Remote component error:', error, errorInfo); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
render() { | ||||||||||||||||||
if (this.state.hasError) { | ||||||||||||||||||
return ( | ||||||||||||||||||
<div style={{ | ||||||||||||||||||
padding: '2em', | ||||||||||||||||||
border: '2px solid #ff6b6b', | ||||||||||||||||||
borderRadius: '4px', | ||||||||||||||||||
backgroundColor: '#ffe0e0', | ||||||||||||||||||
color: '#c92a2a' | ||||||||||||||||||
}}> | ||||||||||||||||||
<h3>⚠️ Component Failed to Load</h3> | ||||||||||||||||||
<p>Unable to load the remote component. Please try again or check the remote application.</p> | ||||||||||||||||||
<details> | ||||||||||||||||||
<summary>Error Details</summary> | ||||||||||||||||||
<pre style={{ fontSize: '12px', overflow: 'auto' }}> | ||||||||||||||||||
{this.state.error?.toString()} | ||||||||||||||||||
</pre> | ||||||||||||||||||
</details> | ||||||||||||||||||
<button | ||||||||||||||||||
onClick={() => this.setState({ hasError: false, error: null })} | ||||||||||||||||||
style={{ | ||||||||||||||||||
marginTop: '1em', | ||||||||||||||||||
padding: '0.5em 1em', | ||||||||||||||||||
backgroundColor: '#c92a2a', | ||||||||||||||||||
color: 'white', | ||||||||||||||||||
border: 'none', | ||||||||||||||||||
borderRadius: '4px', | ||||||||||||||||||
cursor: 'pointer' | ||||||||||||||||||
}} | ||||||||||||||||||
> | ||||||||||||||||||
Retry | ||||||||||||||||||
</button> | ||||||||||||||||||
</div> | ||||||||||||||||||
); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return this.props.children; | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
const getRemoteEntry = (port) => { | ||||||||||||||||||
const baseUrl = process.env.NODE_ENV === 'production' | ||||||||||||||||||
? (process.env.REACT_APP_REMOTE_BASE_URL || window.location.origin) | ||||||||||||||||||
: 'http://localhost'; | ||||||||||||||||||
return `${baseUrl}:${port}/remoteEntry.js`; | ||||||||||||||||||
}; | ||||||||||||||||||
|
||||||||||||||||||
init({ | ||||||||||||||||||
name: 'app1', | ||||||||||||||||||
remotes: [ | ||||||||||||||||||
{ | ||||||||||||||||||
name: 'app2', | ||||||||||||||||||
entry: 'http://localhost:3002/remoteEntry.js', | ||||||||||||||||||
entry: getRemoteEntry(3002), | ||||||||||||||||||
}, | ||||||||||||||||||
{ | ||||||||||||||||||
name: 'app3', | ||||||||||||||||||
entry: 'http://localhost:3003/remoteEntry.js', | ||||||||||||||||||
entry: getRemoteEntry(3003), | ||||||||||||||||||
}, | ||||||||||||||||||
], | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
function useDynamicImport({ module, scope }) { | ||||||||||||||||||
const [component, setComponent] = useState(null); | ||||||||||||||||||
const [loading, setLoading] = useState(false); | ||||||||||||||||||
const [error, setError] = useState(null); | ||||||||||||||||||
|
||||||||||||||||||
useEffect(() => { | ||||||||||||||||||
if (!module || !scope) return; | ||||||||||||||||||
if (!module || !scope) { | ||||||||||||||||||
setComponent(null); | ||||||||||||||||||
setError(null); | ||||||||||||||||||
return; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
const loadComponent = async () => { | ||||||||||||||||||
setLoading(true); | ||||||||||||||||||
setError(null); | ||||||||||||||||||
setComponent(null); | ||||||||||||||||||
|
||||||||||||||||||
try { | ||||||||||||||||||
console.log(`Loading remote module: ${scope}/${module}`); | ||||||||||||||||||
const { default: Component } = await loadRemote(`${scope}/${module}`); | ||||||||||||||||||
setComponent(() => Component); | ||||||||||||||||||
console.log(`Successfully loaded: ${scope}/${module}`); | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Console.log statements should be removed or replaced with a proper logging solution in production code. Consider using a conditional logger or removing debug statements for production builds.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||
} catch (error) { | ||||||||||||||||||
console.error(`Error loading remote module ${scope}/${module}:`, error); | ||||||||||||||||||
setError(error); | ||||||||||||||||||
} finally { | ||||||||||||||||||
setLoading(false); | ||||||||||||||||||
} | ||||||||||||||||||
}; | ||||||||||||||||||
|
||||||||||||||||||
loadComponent(); | ||||||||||||||||||
}, [module, scope]); | ||||||||||||||||||
|
||||||||||||||||||
return component; | ||||||||||||||||||
return { component, loading, error }; | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
function App() { | ||||||||||||||||||
|
@@ -53,7 +129,54 @@ function App() { | |||||||||||||||||
}); | ||||||||||||||||||
}; | ||||||||||||||||||
|
||||||||||||||||||
const Component = useDynamicImport({ module, scope }); | ||||||||||||||||||
const { component: Component, loading, error } = useDynamicImport({ module, scope }); | ||||||||||||||||||
|
||||||||||||||||||
const renderRemoteComponent = () => { | ||||||||||||||||||
if (loading) { | ||||||||||||||||||
return ( | ||||||||||||||||||
<div style={{ | ||||||||||||||||||
padding: '2em', | ||||||||||||||||||
textAlign: 'center', | ||||||||||||||||||
backgroundColor: '#f8f9fa', | ||||||||||||||||||
borderRadius: '4px', | ||||||||||||||||||
border: '2px dashed #dee2e6' | ||||||||||||||||||
}}> | ||||||||||||||||||
<div>🔄 Loading {scope}/{module}...</div> | ||||||||||||||||||
</div> | ||||||||||||||||||
); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if (error) { | ||||||||||||||||||
return ( | ||||||||||||||||||
<div style={{ | ||||||||||||||||||
padding: '2em', | ||||||||||||||||||
border: '2px solid #ffc107', | ||||||||||||||||||
borderRadius: '4px', | ||||||||||||||||||
backgroundColor: '#fff3cd', | ||||||||||||||||||
color: '#856404' | ||||||||||||||||||
}}> | ||||||||||||||||||
<h3>⚠️ Failed to Load Remote Component</h3> | ||||||||||||||||||
<p>Could not load {scope}/{module}</p> | ||||||||||||||||||
<details> | ||||||||||||||||||
<summary>Error Details</summary> | ||||||||||||||||||
<pre style={{ fontSize: '12px', overflow: 'auto', marginTop: '1em' }}> | ||||||||||||||||||
{error.toString()} | ||||||||||||||||||
</pre> | ||||||||||||||||||
</details> | ||||||||||||||||||
</div> | ||||||||||||||||||
); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if (Component) { | ||||||||||||||||||
return ( | ||||||||||||||||||
<ErrorBoundary> | ||||||||||||||||||
<Component /> | ||||||||||||||||||
</ErrorBoundary> | ||||||||||||||||||
); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return null; | ||||||||||||||||||
}; | ||||||||||||||||||
|
||||||||||||||||||
return ( | ||||||||||||||||||
<div | ||||||||||||||||||
|
@@ -68,10 +191,45 @@ function App() { | |||||||||||||||||
The Dynamic System will take advantage of Module Federation <strong>remotes</strong> and{' '} | ||||||||||||||||||
<strong>exposes</strong>. It will not load components that have already been loaded. | ||||||||||||||||||
</p> | ||||||||||||||||||
<button onClick={setApp2}>Load App 2 Widget</button> | ||||||||||||||||||
<button onClick={setApp3}>Load App 3 Widget</button> | ||||||||||||||||||
<div style={{ marginBottom: '1em' }}> | ||||||||||||||||||
<button | ||||||||||||||||||
onClick={setApp2} | ||||||||||||||||||
disabled={loading} | ||||||||||||||||||
style={{ | ||||||||||||||||||
marginRight: '1em', | ||||||||||||||||||
padding: '0.5em 1em', | ||||||||||||||||||
backgroundColor: loading ? '#ccc' : '#007bff', | ||||||||||||||||||
color: 'white', | ||||||||||||||||||
border: 'none', | ||||||||||||||||||
borderRadius: '4px', | ||||||||||||||||||
cursor: loading ? 'not-allowed' : 'pointer' | ||||||||||||||||||
}} | ||||||||||||||||||
> | ||||||||||||||||||
Load App 2 Widget | ||||||||||||||||||
</button> | ||||||||||||||||||
<button | ||||||||||||||||||
onClick={setApp3} | ||||||||||||||||||
disabled={loading} | ||||||||||||||||||
style={{ | ||||||||||||||||||
padding: '0.5em 1em', | ||||||||||||||||||
backgroundColor: loading ? '#ccc' : '#007bff', | ||||||||||||||||||
color: 'white', | ||||||||||||||||||
border: 'none', | ||||||||||||||||||
borderRadius: '4px', | ||||||||||||||||||
cursor: loading ? 'not-allowed' : 'pointer' | ||||||||||||||||||
}} | ||||||||||||||||||
> | ||||||||||||||||||
Load App 3 Widget | ||||||||||||||||||
</button> | ||||||||||||||||||
</div> | ||||||||||||||||||
<div style={{ marginTop: '2em' }}> | ||||||||||||||||||
<Suspense fallback="Loading System">{Component ? <Component /> : null}</Suspense> | ||||||||||||||||||
<Suspense fallback={ | ||||||||||||||||||
<div style={{ padding: '2em', textAlign: 'center' }}> | ||||||||||||||||||
🔄 Initializing component... | ||||||||||||||||||
</div> | ||||||||||||||||||
}> | ||||||||||||||||||
{renderRemoteComponent()} | ||||||||||||||||||
</Suspense> | ||||||||||||||||||
</div> | ||||||||||||||||||
</div> | ||||||||||||||||||
); | ||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
getRemoteEntry
function constructs URLs without proper validation, which could lead to URL injection vulnerabilities. Consider validating the port parameter and sanitizing the baseUrl to ensure it's a valid origin.Copilot uses AI. Check for mistakes.