Skip to content

Instantly share code, notes, and snippets.

@ls-joris-desmedt
Last active July 13, 2020 22:56
Show Gist options
  • Save ls-joris-desmedt/29d297250f84338e82a89458fb30b447 to your computer and use it in GitHub Desktop.
Save ls-joris-desmedt/29d297250f84338e82a89458fb30b447 to your computer and use it in GitHub Desktop.
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;
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;
};
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;
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;
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 };
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;
};
@jherr
Copy link

jherr commented Jul 7, 2020

This is amazing! Can't wait to try it out!

@ScriptedAlchemy
Copy link

very nice!

@Jordan-Gilliam
Copy link

This is beautiful 😍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment