Last active
July 13, 2020 22:56
-
-
Save ls-joris-desmedt/29d297250f84338e82a89458fb30b447 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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; | |
}; |
very nice!
This is beautiful 😍
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is amazing! Can't wait to try it out!