Webpack module federation is not working with eager shared libs
Asked Answered
N

4

28

I was looking into Webpack 5 Module federation feature, and have some trouble understanding why my code does not work. The idea is pretty similar to what standard module federation examples do:

app1 - is the host app app2 - is a remote exposing the whole app to app1

(app1 renders the header and horizontal line, below which the app2 should be rendered)

Both app1 and app2 declares react and react-dom as their shared, singleton, eager dependencies in the weback.config.js:

// app1 webpack.config.js
module.exports = {
  entry: path.resolve(SRC_DIR, './index.js');,
  ...
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      remotes: {
        app2: `app2@//localhost:2002/remoteEntry.js`,
      },
      shared: { react: { singleton: true, eager: true }, "react-dom": { singleton: true, eager: true } },
    }),
    ...
  ],
};
// app2 webpack.config.js
module.exports = {
  entry: path.resolve(SRC_DIR, './index.js');,
  ...
  plugins: [
    new ModuleFederationPlugin({
      name: "app2",
      library: { type: "var", name: "app2" },
      filename: "remoteEntry.js",
      exposes: {
        "./App": "./src/App",
      },
      shared: { react: { singleton: true, eager: true }, "react-dom": { singleton: true, eager: true } },
    }),
    ...
  ],
};

In the App1 index.js I have next code:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";


ReactDOM.render(<App />, document.getElementById("root"));

The App1 App.js component is next:

import React, { Suspense } from 'react';

const RemoteApp2 = React.lazy(() => import("app2/App"));

export default function App() {
  return (
    <div>
      <h1>App 1</h1>
      <p>Below will be some content</p>
      <hr/>
      <Suspense fallback={'Loading App 2'}>
        <RemoteApp2 />
      </Suspense>
    </div>
  );
}

But when I start the application I get the next error:

Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react?1bb3
    at Object.__webpack_modules__.<computed> (consumes:133)
    at __webpack_require__ (bootstrap:21)
    at fn (hot module replacement:61)
    at Module../src/index.js (main.bundle.a8d89941f5dd9a37d429.js:239)
    at __webpack_require__ (bootstrap:21)
    at startup:4
    at startup:6

If I extract everything from index.js to bootstrap.js and in index.js will do

import('./bootstrap');

Everything works just fine.

This confuses me as official docs and blog posts from the creator states that you can do either bootstrap.js way OR declare dependency as an eager one.

Would appreciate any help/insights on why it does not work without bootstrap.js pattern.

Here is a link to full GitHub sandbox I was building: https://github.com/vovkvlad/webpack-module-fedaration-sandbox/tree/master/simple

Nianiabi answered 9/2, 2021 at 16:44 Comment(2)
I am a bit lost at the general concept, in particular: if I have a shell, and different UI's, let's say A and B (microfrontends), it is possible that the shell packs the React, react-dom, materialui dependencies and the A, B packages don't? In my experiments, I see that the bundle size of shell, A and B are the same, so I suspect react, react-dom, etc are being bundled in ALL the apps, beating the goal of reducing bundle size by sharing remote dependencies. thanks for clarifying this!Paginal
@Paginal It is possible. To do it you have to configure Federation plugin to correctly resolve deps. I haven't worked with it for a while, but from what I remember: host app, app A and app B should all have react stated as a shared deps, and required version should meet. In that case it will be loaded only once. Also do not forget to set eager to true for react as there should be only one such lib in the runtime.Nianiabi
A
10

In order to make it work you need to change the way you are loading remote entry.

  1. Update your ModuleFederationPlugin config in webpack.config.js for app1 to this:
...

new ModuleFederationPlugin({
    name: "app1",
    remoteType: 'var',
    remotes: {
      app2: 'app2',
    },
    shared: {
      ...packageJsonDeps,
      react: { singleton: true, eager: true, requiredVersion: packageJsonDeps.react },
      "react-dom": { singleton: true, eager: true, requiredVersion: packageJsonDeps["react-dom"] }
    },
}),

...
  1. Add script tag to the head of your index.html in app1 :
<script src="http://localhost:2002/remoteEntry.js"></script>

Good look with further hacking!

UPDATE:

Just for the sake of it: I've create a PR to your sandbox repo with fixes described above: https://github.com/vovkvlad/webpack-module-fedaration-sandbox/pull/2

Anathema answered 15/3, 2021 at 0:18 Comment(5)
So just to make things clear clear: your proposed changes do work, but as far as I understand from playing around with it, is that the bottom line is that the remoteEntry.js should be loaded BEFORE the bundle that actually runs the app. And that's basically, what bootstrap.js does - it makes the main app to load after main_bundle and remoteEntry.js When using your proposed change, it does the same - it downloads remoteEntry.js before the main_bundle as main_bundle is being added by webpack after hardcoded script tag with remoteEntry.jsNianiabi
Here is a link with screenshots of all three situations: imgur.com/a/63WTCMgNianiabi
I'm having the same problem. And the suggested solution does not scale, if we are talking about development or a POC is ok, but when you have production-ready applications with 3+ environments this solution will not work. If anyone has a solution that works with dynamic imports, I'm really interested how you solved this issue.Gingersnap
Is there any news regarding a solution without bootstrap file or add scripts to all remote entry?Borden
can anyone help here #75959091Jasonjasper
N
20

Just to make it clear for those who might miss the comment to the initial answer:

It seems like the main reason of why it failed initially was that remoteEntry.js file was loaded after the code that actually runs the host app.

Both bootstrap.js approach and adding direct script <script src="http://localhost:2002/remoteEntry.js"></script> to the <head></head> tag has exactly the same outcome - they make remoteEntry.js be loaded and parsed BEFORE the main app's code.

In case of bootstrap the order is next:

  1. main_bundle is loaded
  2. as the main code is extracted into bootstrap.js file - remoteEntry.js is loaded
  3. bootstrap.js is loaded which actually runs the main app

enter image description here

with proposed variant by Oleg Vodolazsky events order is next:

  1. remoteEntry.js is loaded first as it is directly added to html file and webpack's main_bundle is being appended to <head></head> after remoteEntry link
  2. main_bundle is loaded and runs the application

enter image description here

and in case of just trying to run app without bootstrap and without hardcoded script in the <head></head> main_bundle is loaded before remoteEntry.js and as main_bundle tries to actually run the app, it fails with an error:

enter image description here

Nianiabi answered 15/3, 2021 at 13:28 Comment(0)
A
10

In order to make it work you need to change the way you are loading remote entry.

  1. Update your ModuleFederationPlugin config in webpack.config.js for app1 to this:
...

new ModuleFederationPlugin({
    name: "app1",
    remoteType: 'var',
    remotes: {
      app2: 'app2',
    },
    shared: {
      ...packageJsonDeps,
      react: { singleton: true, eager: true, requiredVersion: packageJsonDeps.react },
      "react-dom": { singleton: true, eager: true, requiredVersion: packageJsonDeps["react-dom"] }
    },
}),

...
  1. Add script tag to the head of your index.html in app1 :
<script src="http://localhost:2002/remoteEntry.js"></script>

Good look with further hacking!

UPDATE:

Just for the sake of it: I've create a PR to your sandbox repo with fixes described above: https://github.com/vovkvlad/webpack-module-fedaration-sandbox/pull/2

Anathema answered 15/3, 2021 at 0:18 Comment(5)
So just to make things clear clear: your proposed changes do work, but as far as I understand from playing around with it, is that the bottom line is that the remoteEntry.js should be loaded BEFORE the bundle that actually runs the app. And that's basically, what bootstrap.js does - it makes the main app to load after main_bundle and remoteEntry.js When using your proposed change, it does the same - it downloads remoteEntry.js before the main_bundle as main_bundle is being added by webpack after hardcoded script tag with remoteEntry.jsNianiabi
Here is a link with screenshots of all three situations: imgur.com/a/63WTCMgNianiabi
I'm having the same problem. And the suggested solution does not scale, if we are talking about development or a POC is ok, but when you have production-ready applications with 3+ environments this solution will not work. If anyone has a solution that works with dynamic imports, I'm really interested how you solved this issue.Gingersnap
Is there any news regarding a solution without bootstrap file or add scripts to all remote entry?Borden
can anyone help here #75959091Jasonjasper
J
2

You can set the dependency as eager inside the advanced API of Module Federation, which doesn’t put the modules in an async chunk, but provides them synchronously. This allows us to use these shared modules in the initial chunk. But be careful as all provided and fallback modules will always be downloaded. It’s recommended to provide it only at one point of your application, e.g. the shell.

Webpack's website strongly recommend using an asynchronous boundary. It will split out the initialization code of a larger chunk to avoid any additional round trips and improve performance in general.

For example, your entry looked like this:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

Let's create bootstrap.js file and move contents of the entry into it, and import that bootstrap into the entry:

index.js

+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));

bootstrap.js

+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));

This method works but can have limitations or drawbacks.

Setting eager: true for dependency via the ModuleFederationPlugin

webpack.config.js

// ...
new ModuleFederationPlugin({
  shared: {
    ...deps,
    react: {
      eager: true,
    },
  },
});

Source

Jenette answered 29/8, 2021 at 8:2 Comment(3)
Besides import(".bootstrap") are the remaining imports and ReactDom.Render() necessary in the index.js file?Erdrich
@ChunkyChunk No it isn't necessary. Because we have it in bootstrap file.Jenette
I didn't notice the "+" and "-". Now I understand what you wrote.Erdrich
K
2

I also faced with problems when using NextJS app as a container app. You know, we had to give eager loaded again and again.

I followed a different way from current techniques on the web. I used dynamic load technique for my remote library and it seems shared modules are not being fetched again and again now. They are being loaded only once. Also, I implemented full system as framework agnostic so you can use any framework as a remote app (angular, vue, react, svelte...). Also, I moved SSR logic to remote app part so now it can fully support SSR for any framework, too. It seems worked and I wanted to share my solution in here to help community. I wrote a medium blog post with example Github repo link. I told my approach with details.

You can find detailed post in here: https://medium.com/@metinarslanturkk/how-i-implemented-dynamic-loaded-framework-agnostic-microfrontend-app-with-nextjs-and-react-which-620ff3df4298

And this is the example repo link: https://github.com/MetinArslanturk/microfrontend-nextjs-ssr

Thanks, Metin

Kallick answered 22/1, 2022 at 9:29 Comment(1)
Very interesting stuff! You really put a lot of work into this. Thanks for the guide and the git example. I actually looked at your code for quite a while, mostly next-host. Using it, I was finally able to load my react remote without getting the "Invalid hook" messageRudderpost

© 2022 - 2024 — McMap. All rights reserved.