Skip to content

Instantly share code, notes, and snippets.

@ScriptedAlchemy
Last active October 16, 2024 22:52
Show Gist options
  • Save ScriptedAlchemy/3a24008ef60adc47fad1af7d3299a063 to your computer and use it in GitHub Desktop.
Save ScriptedAlchemy/3a24008ef60adc47fad1af7d3299a063 to your computer and use it in GitHub Desktop.
The Right Way to Load Dynamic Remotes
import { injectScript } from '@module-federation/utilities';
// example of dynamic remote import on server and client
const isServer = typeof window === 'undefined';
//could also use
// getModule({
// remoteContainer: {
// global: 'app2',
// url: 'http://localhost:3002/remoteEntry.js',
// },
// modulePath: './sample'
// }).then((sample) => {
// console.log(sample)
// });
const dynamicContainer = injectScript({
global: 'checkout',
url: `http://localhost:3002/_next/static/${
isServer ? 'ssr' : 'chunks'
}/remoteEntry.js`,
}).then((container) => {
return container.get('./CheckoutTitle').then((factory) => {
return factory();
});
});
// if you wanted to use it server side/client side in next.js
const DynamicComponent = React.lazy(() => dynamicContainer);
// eslint-disable-next-line react/display-name
export default (props) => {
return (
<>
<React.Suspense>
<DynamicComponent />
</React.Suspense>
<p>Code from GSSP:</p>
<pre>{props.code}</pre>
</>
);
};
export async function getServerSideProps() {
return {
props: {
code: (await dynamicContainer).default.toString(),
},
};
}
@marcog83
Copy link

marcog83 commented Sep 7, 2022

Hi, i wrapped onload function into try/catch, so that i can reject the promise if somethings goes wrong (Ex remote is not in window).
Does it make sense for you? :)

      // when remote is loaded..
      const onload = async () => {
        try {
          // check if it was initialized
          if (!window[remote].__initialized) {
            // if share scope doesnt exist (like in webpack 4) then expect shareScope to be a manual object
            if (typeof __webpack_share_scopes__ === 'undefined') {
              // use default share scope object passed in manually
              await window[remote].init(shareScope.default);
            } else {
              // otherwise, init share scope as usual
              await window[remote].init(__webpack_share_scopes__[shareScope]);
            }

            // mark remote as initialized
            // eslint-disable-next-line require-atomic-updates
            window[remote].__initialized = true;
          }

          // resolve promise so marking remote as loaded
          resolve();
        } catch (e) {
          reject(e);
        }
      };

Rejecting the promise allows me to use an ErrorBoundary when i load a Federated Module dynamically

 const { Component: FederatedComponent } = useFederatedComponent(url, scope, module);
<ErrorBoundary>
      <Suspense fallback={ fallback }>
        { FederatedComponent && <FederatedComponent /> }
      </Suspense>
    </ErrorBoundary>

@ScriptedAlchemy
Copy link
Author

@marcog83
Copy link

WoW,thanks for quick answer... I'll have a look and I'll update my loader. thanks. You are doing an incredible job with module federation. 💪💥💯💯🔝

@ScriptedAlchemy
Copy link
Author

Much appreciated ❤️

@shirly-chen-awx
Copy link

shirly-chen-awx commented Sep 23, 2022

Hi, @ScriptedAlchemy , for existingRemote, I think we can use event emit and listen rather than overloading the onload function in existingRemote block.
problematic scenario:

  1. I call the getOrLoadRemote twice in a very short time.
  2. The first getOrLoadRemote will go to create the script and listen to the onload function
  3. The second getOrLoadRemote comes and the script exists and the onload function the first time assigned hasn't been executed. The script will be replaced with another onload function. It will lead to the first getOrLoadRemote won't be resolved forever.

This is my suggestion below:

/**
 *
 * @param {string} remote - the remote global name
 * @param {object | string} shareScope - the shareScope Object OR scope key
 * @param {string} remoteFallbackUrl - fallback url for remote module
 * @returns {Promise<object>} - Federated Module Container
 */
export const getOrLoadRemote = (remote: string, shareScope: string, remoteFallbackUrl?: string): Promise<object> =>
  new Promise((resolve, reject) => {
    // check if remote exists on window
    if (!window[remote]) {
      // search dom to see if remote tag exists, but might still be loading (async)
      const existingRemote = document.querySelector(`[data-webpack="${remote}"]`);
      // when remote is loaded.
      const onload = async () => {
        await __webpack_init_sharing__('default');
        // check if it was initialized
        if (!window[remote]?.__initialized) {
          // if share scope doesnt exist (like in webpack 4) then expect shareScope to be a manual object
          if (typeof __webpack_share_scopes__ === 'undefined') {
            // use default share scope object passed in manually
            await window[remote].init(shareScope.default);
          } else {
            // otherwise, init share scope as usual
            await window[remote].init(__webpack_share_scopes__[shareScope]);
          }
          // mark remote as initialized
          window[remote].__initialized = true;
        }
         // my suggestion code::::::
        const script = document.querySelector(`[data-webpack="${remote}"]`);
        const loaded = new CustomEvent('loaded');
        script?.dispatchEvent(loaded);

        // resolve promise so marking remote as loaded
        resolve();
      };
      if (existingRemote) {
         // my suggestion code:::::
         existingRemote.addEventListener('loaded', () => {
          resolve();
        });
        // check if remote fallback exists as param passed to function
        // TODO: should scan public config for a matching key if no override exists
      } else if (remoteFallbackUrl) {
        // inject remote if a fallback exists and call the same onload function
        const script = document.createElement('script');
        script.type = 'text/javascript';
        // mark as data-webpack so runtime can track it internally
        script.setAttribute('data-webpack', `${remote}`);
        script.async = true;
        script.onerror = reject;
        script.onload = onload;
        script.src = remoteFallbackUrl;
        document.getElementsByTagName('head')[0].appendChild(script);
      } else {
        // no remote and no fallback exist, reject
        reject(`Cannot Find Remote ${remote} to inject`);
      }
    } else {
      // remote already instantiated, resolve
      resolve();
    }
  });

@ScriptedAlchemy
Copy link
Author

Don’t do manual script injection. Webpack has its own script loading function you can access. Check injectScript function in nextjs-MF repo.

@shirly-chen-awx
Copy link

shirly-chen-awx commented Sep 26, 2022

It's a nice improvement🤩, much thanks💜💜💜!!! One more question, can we use injectScript directly through npm package now?

@ScriptedAlchemy
Copy link
Author

I export it as a utility so you can. I’ll be moving it to module-federation/utilities and pushing to npm in near future

@ScriptedAlchemy
Copy link
Author

its on npm as module-federation/utilities now, readme needs some love -- but its there if you want it.

Could use a PR or two as I've not tested it as a standalone util

@shirly-chen-awx
Copy link

wowww😍, thank you very much for your tireless efforts.

@shirly-chen-awx
Copy link

Hi, @ScriptedAlchemy I found that injectScript function works in dev env, but it will throw __webpack_require__.l is not a function error in prod env. do you have any thoughts?

@Hydrock
Copy link

Hydrock commented Oct 27, 2022

Hello. I need your help. I recorded a video (https://www.youtube.com/watch?v=CMA6WciiGso) where I show the problem.

The problem is the following.

I used the function from the post to create a "ComponentLoader" - a special component for loading remote components

export function ComponentLoader(props) {
    console.log('0 ComponentLoader - componentName:', props.componentName);
    const {
        componentName,
        ...restProps
    } = props;

    const system = {
        remote: 'Shared',
        url: 'http://localhost:8081/assets/remoteEntry.js',
        module: `./${componentName}`,
    }
    
    if (typeof window !== 'undefined') {
        const Component = React.lazy(loadComponent(system.remote, 'default', system.module, system.url));

        return (
            <ErrorBoundary errorResetFlag={ componentName } >
                <Suspense fallback="loading !">
                    <Component
                        widgetName={ componentName }
                        {...restProps}
                    />
                </Suspense>
            </ErrorBoundary>
        );
    }

    return null;
}
import { getOrLoadRemote } from './getOrLoadRemote';

export const loadComponent = (remote, sharedScope, module, url) => {
  return async () => {
    await getOrLoadRemote(remote, sharedScope, url);
    const container = window[remote];
    const factory = await container.get(module);
    const Module = factory();
    return Module;
  };
};
export const getOrLoadRemote = (remote, shareScope, remoteFallbackUrl = undefined) =>
 new Promise((resolve, reject) => {
   // check if remote exists on window
   if (!window[remote]) {
     // search dom to see if remote tag exists, but might still be loading (async)
     const existingRemote = document.querySelector(`[data-webpack="${remote}"]`);
     // when remote is loaded..
     const onload = originOnload => async () => {
       // check if it was initialized
       if (!window[remote].__initialized) {
         // if share scope doesnt exist (like in webpack 4) then expect shareScope to be a manual object
         if (typeof __webpack_share_scopes__ === 'undefined') {
           // use default share scope object passed in manually
           await window[remote].init(shareScope.default);
         } else {
           // otherwise, init share scope as usual
           await window[remote].init(__webpack_share_scopes__[shareScope]);
         }
         // mark remote as initialized
         window[remote].__initialized = true;
       }
       // resolve promise so marking remote as loaded
       resolve();
       originOnload && originOnload();
     };
     if (existingRemote) {
       // if existing remote but not loaded, hook into its onload and wait for it to be ready
       existingRemote.onload = onload(existingRemote.onload);
       existingRemote.onerror = reject;
       // check if remote fallback exists as param passed to function
       // TODO: should scan public config for a matching key if no override exists
     } else if (remoteFallbackUrl) {
       // inject remote if a fallback exists and call the same onload function
       var d = document,
         script = d.createElement('script');
       script.type = 'text/javascript';
       // mark as data-webpack so runtime can track it internally
       script.setAttribute('data-webpack', `${remote}`);
       script.async = true;
       script.onerror = reject;
       script.onload = onload(null);
       script.src = remoteFallbackUrl;
       d.getElementsByTagName('head')[0].appendChild(script);
     } else {
       // no remote and no fallback exist, reject
       reject(`Cannot Find Remote ${remote} to inject`);
     }
   } else {
     // remote already instantiated, resolve
     resolve();
   }
 });

I use it here:

const sharedComponentsNames = {
    ExampleSharedComponent1: 'ExampleSharedComponent1',
    ExampleSharedComponent2: 'ExampleSharedComponent2',
}

export function ExternalComponentExample(props) {
    console.log('-1 ExternalComponentExampleContainer:', props);

    const { ComponentLoader } = window;

    const dynamicComponentName = sharedComponentsNames.ExampleSharedComponent2;
    
    return (
        <div>
            <div>
                <h1>
                    { dynamicComponentName }
                </h1><br />
                <ComponentLoader
                    componentName={ dynamicComponentName }
                />
            </div>
        </div>
    );
}

everything works fine when I load components at the same level
but when a downloadable component also has a downloadable component - this leads to a lot of re-rendering
it is worth noting that re-rendering is not infinite and sooner or later we all still see the final result

this is how the downloadable components look like

export default function ExampleSharedComponent1(props) {
  console.log('1 ExampleSharedComponent1:', props);
  return (
    <div>
      ExampleSharedComponent_____________1
    </div>
  );
}
export default function ExampleSharedComponent2(props) {
  console.log('1 ExampleSharedComponent2:', props);

  const { ComponentLoader } = window;

  return (
    <div>
      ExampleSharedComponent________2

      <ComponentLoader
          componentName={ 'ExampleSharedComponent1' }
      />
    </div>
  );
}

I'm new to wmf - so I really hope for your help
@ScriptedAlchemy 😢

@Hydrock
Copy link

Hydrock commented Oct 30, 2022

I think, i found answer in this article - https://dev.to/omher/lets-dynamic-remote-modules-with-webpack-module-federation-2b9m
At the same time, there is no re-rendering with nested loading of modular components.

@oravecz
Copy link

oravecz commented Apr 27, 2023

@shirly-chen-awx Did you ever find a solution to the __webpack_require__.l is not a function? We are experiencing the same strange behavior.

@ScriptedAlchemy
Copy link
Author

ensure the file is getting bundled by webpack and not treated as external. @oravecz

that error happens when something is importing it outside webpacks scope

@oravecz
Copy link

oravecz commented Apr 28, 2023

Yes, it was a duplicate of module-federation/core#551

I'm surprised it isn't discussed more, especially on the @module-federation/utilities page. importRemote is the function that is calling __webpack_require__.l(), and I would expect any production build using that package would tree-shake away the function without that plugin.

Is this plugin available as an npm published webpack plugin at this time?

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