-
-
Save Jordan-Gilliam/85572a324b09c71d943792c3d791b7b8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { FederatedProvider } from './federated-provider'; | |
import { scopes } from './scopes'; | |
// This is an example app on how you would setup your Nextjs app | |
const App = ({ Component }) => { | |
return ( | |
<FederatedProvider scopes={scopes}> | |
<Component /> | |
</FederatedProvider> | |
); | |
}; | |
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
createContext, | |
ReactNode, | |
useState, | |
useCallback, | |
useContext, | |
useEffect, | |
} from 'react'; | |
import { RemoteMap } from './scopes'; | |
import { initiateComponent } from './utils'; | |
// This is the federated provider, it keeps some date about which scopes/modules are already initiated/loaded | |
// This way we don't have to do this twice if we reload an already initiated/loaded scope/module | |
// It provides a callback function to load the actual module | |
interface State { | |
scopes: { [key: string]: true }; | |
components: { [key: string]: any }; | |
} | |
const federatedContext = createContext< | |
State & { loadComponent: (scope: string, module: string) => void } | |
>({ scopes: {}, components: {}, loadComponent: () => {} }); | |
export const FederatedProvider = ({ | |
children, | |
scopes, | |
}: { | |
children: ReactNode; | |
scopes: RemoteMap; | |
}) => { | |
const [state, setState] = useState<State>({ scopes: {}, components: {} }); | |
const loadComponent = useCallback( | |
async (scope: string, module: string) => { | |
if (!state.scopes[scope]) { | |
await scopes[scope].initiate(global, scope, scopes[scope].remote); | |
const component = initiateComponent(global, scope, module); | |
setState((currentState) => ({ | |
...currentState, | |
scopes: { ...currentState.scopes, [scope]: true }, | |
components: { ...currentState.components, [`${scope}-${module}`]: component }, | |
})); | |
} | |
if (!state.components[`${scope}-${module}`]) { | |
const component = initiateComponent(global, scope, module); | |
setState((currentState) => ({ | |
...currentState, | |
components: { ...currentState.components, [`${scope}-${module}`]: component }, | |
})); | |
} | |
}, | |
[state, scopes], | |
); | |
return ( | |
<federatedContext.Provider value={{ ...state, loadComponent }}> | |
{children} | |
</federatedContext.Provider> | |
); | |
}; | |
// This is a hook to use in your component to get the actual module | |
// It hides all the module federation logic that is happening | |
export const useFederatedComponent = (scope: string, module: string) => { | |
const { components, loadComponent } = useContext(federatedContext); | |
const component = components[`${scope}-${module}`]; | |
useEffect(() => { | |
if (!component) { | |
loadComponent(scope, module); | |
} | |
}, [component, scope, module, loadComponent]); | |
if (!component) { | |
return () => null; | |
} | |
return component; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import RemoteComponent from './remote-component'; | |
// An example of how we would we would use a remote component in a page | |
const Page = () => { | |
return ( | |
<> | |
<RemoteComponent scope="peer" module="./component1" props={{ value: foo }} /> | |
<RemoteComponent scope="peer" module="./component2" props={{}} /> | |
</> | |
); | |
}; | |
export default Page; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { useFederatedComponent } from './federated-provider'; | |
// This is a component to easily consume remote components, just provide the scope name and module name | |
// Make sure that the scope is defined in the federated provider `scopes` value | |
const RemoteComponent = ({ | |
scope, | |
module, | |
props, | |
}: { | |
scope: string; | |
module: string; | |
props?: any; | |
}) => { | |
const Component = useFederatedComponent(scope, module); | |
const loading = <div>Loading...</div>; | |
if (typeof window === 'undefined') { | |
return loading; | |
} | |
return ( | |
<React.Suspense fallback={loading}> | |
<Component {...props} /> | |
</React.Suspense> | |
); | |
}; | |
export default RemoteComponent; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { initiateRemote, initiateScope } from './utils'; | |
// This is an example of how a scope configuration would look like | |
// You can here define all the remote scopes your application needs | |
// These will lazily initiated and only when needed | |
// With this you can define a different set of shared libs for each scope | |
export interface RemoteScope { | |
remote: string; | |
initiate: (scope: any, scopeName: string, remote: string) => Promise<void>; | |
} | |
export interface RemoteMap { | |
[key: string]: RemoteScope; | |
} | |
const peerScope = { | |
remote: 'http://localhost:8080/remoteEntry.js', | |
initiate: async (scope: any, scopeName: string, remote: string) => { | |
await initiateRemote(remote); | |
initiateScope(scope, scopeName, () => ({ | |
react: { | |
get: () => Promise.resolve(() => require('react')), | |
loaded: true, | |
}, | |
'emotion-theming': { | |
get: () => Promise.resolve(() => require('emotion-theming')), | |
loaded: true, | |
}, | |
'@emotion/core': { | |
get: () => Promise.resolve(() => require('@emotion/core')), | |
loaded: true, | |
}, | |
'@emotion/styled': { | |
get: () => Promise.resolve(() => require('@emotion/styled')), | |
loaded: true, | |
}, | |
})); | |
}, | |
}; | |
export const scopes: RemoteMap = { peer: peerScope }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
// These are some utility functions you can use to initiate remotes/scopes/modules | |
export const initiateRemote = (remote: string): Promise<void> => { | |
return new Promise((resolve, reject) => { | |
const existingScript = document.querySelector(`script[src="${remote}"]`); | |
if (existingScript) { | |
existingScript.addEventListener('load', () => { | |
resolve(); | |
}); | |
return; | |
} | |
const element = document.createElement('script'); | |
element.src = remote; | |
element.type = 'text/javascript'; | |
element.async = true; | |
element.onload = () => { | |
console.log(`Dynamic Script Loaded: ${remote}`); | |
resolve(); | |
}; | |
element.onerror = () => { | |
console.error(`Dynamic Script Error: ${remote}`); | |
reject(); | |
}; | |
document.head.appendChild(element); | |
}); | |
}; | |
export const initiateScope = (scopeObject: any, scopeName: string, sharedLibs: () => any) => { | |
if (scopeObject[scopeName] && scopeObject[scopeName].init) { | |
try { | |
scopeObject[scopeName].init( | |
Object.assign( | |
sharedLibs(), | |
// eslint-disable-next-line | |
// @ts-ignore | |
scopeObject.__webpack_require__ ? scopeObject.__webpack_require__.o : {}, | |
), | |
); | |
} catch (err) { | |
// It can happen due to race conditions that we initialise the same scope twice | |
// In this case we swallow the error | |
if ( | |
err.message !== | |
'Container initialization failed as it has already been initialized with a different share scope' | |
) { | |
throw err; | |
} else { | |
console.log('SWALLOWING INIT ERROR'); | |
} | |
} | |
} else { | |
throw new Error(`Could not find scope ${scopeName}`); | |
} | |
}; | |
export const initiateComponent = (scope: any, scopeName: string, module: string) => { | |
const component = React.lazy(() => | |
scope[scopeName].get(module).then((factory) => { | |
const Module = factory(); | |
return Module; | |
}), | |
); | |
return component; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment