Skip to content

Instantly share code, notes, and snippets.

@ls-joris-desmedt
Last active July 13, 2020 22:56
Show Gist options
  • Select an option

  • Save ls-joris-desmedt/29d297250f84338e82a89458fb30b447 to your computer and use it in GitHub Desktop.

Select an option

Save ls-joris-desmedt/29d297250f84338e82a89458fb30b447 to your computer and use it in GitHub Desktop.

Revisions

  1. ls-joris-desmedt revised this gist Jul 7, 2020. 7 changed files with 233 additions and 99 deletions.
    15 changes: 15 additions & 0 deletions _app.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    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;
    80 changes: 80 additions & 0 deletions federated-provider.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,80 @@
    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;
    };
    15 changes: 15 additions & 0 deletions page.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    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;
    63 changes: 12 additions & 51 deletions remote-component.tsx
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,8 @@
    import React, { useState, useEffect } from 'react';
    import { useDynamicScript } from 'use-dynamic-script';
    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,
    @@ -8,61 +11,19 @@ const RemoteComponent = ({
    }: {
    scope: string;
    module: string;
    props: any;
    props?: any;
    }) => {
    const { ready, failed } = useDynamicScript('http://localhost:8080/remoteEntry.js');
    const [Component, setComponent] = useState(null);

    useEffect(() => {
    if (ready && !failed && !Component) {
    if (global[scope] && global[scope].init) {
    global[scope].init(
    Object.assign(
    {
    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,
    },
    },
    // eslint-disable-next-line
    // @ts-ignore
    global.__webpack_require__ ? global.__webpack_require__.o : {},
    ),
    );

    const Component = React.lazy(() =>
    global[scope].get(module).then((factory) => {
    const Module = factory();
    return Module;
    }),
    );

    setComponent(Component);
    }
    }
    }, [ready, failed, initialised, scope]);
    const Component = useFederatedComponent(scope, module);
    const loading = <div>Loading...</div>;

    if (!ready || failed || !Component || !global) {
    return null;
    if (typeof window === 'undefined') {
    return loading;
    }

    return (
    <React.Suspense fallback={<div>Loading...</div>}>
    <React.Suspense fallback={loading}>
    <Component {...props} />
    </React.Suspense>
    );
    };

    export default RemoteComponent;
    export default RemoteComponent;
    42 changes: 42 additions & 0 deletions scope.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    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 };
    48 changes: 0 additions & 48 deletions use-dynamic-script.tsx
    Original file line number Diff line number Diff line change
    @@ -1,48 +0,0 @@
    import React, { useState, useEffect } from 'react';


    export const useDynamicScript = (url: string) => {
    const [ready, setReady] = React.useState(false);
    const [failed, setFailed] = React.useState(false);

    React.useEffect(() => {
    if (!url) {
    return;
    }

    if (document.querySelector(`script[src="${url}"]`)) {
    setReady(true);
    }

    const element = document.createElement('script');
    element.src = url;
    element.type = 'text/javascript';
    element.async = true;

    setReady(false);
    setFailed(false);

    element.onload = () => {
    console.log(`Dynamic Script Loaded: ${url}`);
    setReady(true);
    };

    element.onerror = () => {
    console.error(`Dynamic Script Error: ${url}`);
    setReady(false);
    setFailed(true);
    };

    document.head.appendChild(element);

    return () => {
    console.log(`Dynamic Script Removed: ${url}`);
    document.head.removeChild(element);
    };
    }, [url]);

    return {
    ready,
    failed,
    };
    };
    69 changes: 69 additions & 0 deletions utils.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,69 @@
    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;
    };
  2. ls-joris-desmedt revised this gist Jul 6, 2020. 1 changed file with 12 additions and 11 deletions.
    23 changes: 12 additions & 11 deletions remote-component.tsx
    Original file line number Diff line number Diff line change
    @@ -11,10 +11,10 @@ const RemoteComponent = ({
    props: any;
    }) => {
    const { ready, failed } = useDynamicScript('http://localhost:8080/remoteEntry.js');
    const [initialised, setInitialised] = useState(false);
    const [Component, setComponent] = useState(null);

    useEffect(() => {
    if (ready && !failed && !initialised) {
    if (ready && !failed && !Component) {
    if (global[scope] && global[scope].init) {
    global[scope].init(
    Object.assign(
    @@ -41,22 +41,23 @@ const RemoteComponent = ({
    global.__webpack_require__ ? global.__webpack_require__.o : {},
    ),
    );
    setInitialised(true);

    const Component = React.lazy(() =>
    global[scope].get(module).then((factory) => {
    const Module = factory();
    return Module;
    }),
    );

    setComponent(Component);
    }
    }
    }, [ready, failed, initialised, scope]);

    if (!ready || failed || !initialised || !global) {
    if (!ready || failed || !Component || !global) {
    return null;
    }

    const Component = React.lazy(() =>
    global[scope].get(module).then((factory) => {
    const Module = factory();
    return Module;
    }),
    );

    return (
    <React.Suspense fallback={<div>Loading...</div>}>
    <Component {...props} />
  3. ls-joris-desmedt created this gist Jul 6, 2020.
    67 changes: 67 additions & 0 deletions remote-component.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,67 @@
    import React, { useState, useEffect } from 'react';
    import { useDynamicScript } from 'use-dynamic-script';

    const RemoteComponent = ({
    scope,
    module,
    props,
    }: {
    scope: string;
    module: string;
    props: any;
    }) => {
    const { ready, failed } = useDynamicScript('http://localhost:8080/remoteEntry.js');
    const [initialised, setInitialised] = useState(false);

    useEffect(() => {
    if (ready && !failed && !initialised) {
    if (global[scope] && global[scope].init) {
    global[scope].init(
    Object.assign(
    {
    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,
    },
    },
    // eslint-disable-next-line
    // @ts-ignore
    global.__webpack_require__ ? global.__webpack_require__.o : {},
    ),
    );
    setInitialised(true);
    }
    }
    }, [ready, failed, initialised, scope]);

    if (!ready || failed || !initialised || !global) {
    return null;
    }

    const Component = React.lazy(() =>
    global[scope].get(module).then((factory) => {
    const Module = factory();
    return Module;
    }),
    );

    return (
    <React.Suspense fallback={<div>Loading...</div>}>
    <Component {...props} />
    </React.Suspense>
    );
    };

    export default RemoteComponent;
    48 changes: 48 additions & 0 deletions use-dynamic-script.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    import React, { useState, useEffect } from 'react';


    export const useDynamicScript = (url: string) => {
    const [ready, setReady] = React.useState(false);
    const [failed, setFailed] = React.useState(false);

    React.useEffect(() => {
    if (!url) {
    return;
    }

    if (document.querySelector(`script[src="${url}"]`)) {
    setReady(true);
    }

    const element = document.createElement('script');
    element.src = url;
    element.type = 'text/javascript';
    element.async = true;

    setReady(false);
    setFailed(false);

    element.onload = () => {
    console.log(`Dynamic Script Loaded: ${url}`);
    setReady(true);
    };

    element.onerror = () => {
    console.error(`Dynamic Script Error: ${url}`);
    setReady(false);
    setFailed(true);
    };

    document.head.appendChild(element);

    return () => {
    console.log(`Dynamic Script Removed: ${url}`);
    document.head.removeChild(element);
    };
    }, [url]);

    return {
    ready,
    failed,
    };
    };