Pass the Remote – Hack Day 2021

Module Federation is a new feature in webpack 5 that allows you to pull dependencies from external sources. Correctly configured, those imports can be dynamic, shared between multiple apps, and pull in any kind of module (utility, component or feature). What’s more, Module Federation comes with a host of comprehensive examples for many frameworks.

This introduces a lot of options for designing and building software:

  • Externalised dependencies with their own builds, and bundles
  • An import structure for composable “micro frontends”
  • Hooks for customisation of just about anything (and the corresponding challenges, of course)

We were interested in Module Federation as a means of providing component customisation hooks to our clients. Our initial plan was that they would provide a good way to pull custom widgets or maybe features into some of our apps.

What we learned from using them is that they work for that purpose, but also for many more. Federation allows you to structure apps differently, and more dynamically. This enables some new ways of working beyond what we’d imagined, as well as providing an alternate route for some of our other improvements to the code base.

Actual Hackday Stuff

Our hackday goal itself was modest: implement a mechanism to load remote components. If we could achieve that, we also set ourselves a couple of stretch goals, such as integrating those components into our Redux store, gaining first class access to our application packages and styling the components with our design system.

We spent some time studying and reading up on Module Federation. But once we were happy with the approach, we managed to break down our goals into a few tasks:

  • Migrate one of our test beds from Webpack v4 to Webpack v5
  • Implement a basic remote environment and build that we could use to serve our remote component
  • Create a remote component loader in React, following the advanced examples
  • Integrate the loader into our test bed
  • Make the loader work with state, and have it pass messages between itself and the parent app

Happily, we managed to achieve all of our goals. There were definitely a few more advanced cases we wanted to try and build out, but we managed to achieve a major stretch goal by getting a remote to fully interact with the app. We were able to pull in a remote, and hook it into some existing workflows for charting, trading, and Post Trade actions.

This is a testament to how easy to use the Module Federation system is. The webpack config is light and easily exposed via a webpack plugin, and we were already comfortable using dynamic imports and React Suspense to handle the loading of Remote modules.

// Main application configuration
const { ModuleFederationPlugin } = require("webpack").container

webpackConfig.plugins.push(
  new ModuleFederationPlugin({
    name: "main",
    shared: {
      react: { singleton: true },
      "react-dom": { singleton: true },
      "react-redux": { singleton: true },
      "redux-trading": { singleton: true },
      "common-library-components": { singleton: true },
    },
  })
);
// Remote application configuration
webpackConfig.plugins.push(
  new ModuleFederationPlugin({
    name: "remote",
    filename: "remoteEntry.js",
    exposes: {
      "./Table": "./src/Table.js",
    },
    shared: {
      react: { singleton: true },
      "react-dom": { singleton: true },
    },
  })
);

Most of the hard work was upgrading to webpack 5, and pulling together a remote app that could do everything we wanted it to (more on this later).

An idea we’ve been exploring is moving more packages out of a monorepo and into a package registry (say, npm). But with federation, we can take that a step further, and just publish modules directly. This would make publishing changes much more efficient, as well as reducing the size of our bundles! It’ll be interesting to see how this ecosystem evolves. Hosting federated packages seems like a service that existing package registries could provide. It’s already being explored with Third Party Modules in Deno.

Network requests for the remote component bundles

We think there are still some challenges to overcome there – especially around secure implementations, and versioning releases. But it’s certainly something worth exploring.

There were several takeaways from our work:

Natural hooks and nice things

Our stretch goal was to try and integrate a remote component with our application.

We found React contexts made this easy and natural. We discovered that we could wrap remote React components with existing providers, and provide the components with:

  • Global Redux State
  • Styled Components Theming
  • Caplin StreamLink (our streaming messaging library)

This makes integration of any data or hooks that are in a context effortless, and these items can then be provided as an API to remotes.

The remote React component has seamless access to the app shell context data:

ReactDOM.render(
  <Provider store={this.store}>
    <UserProvider value={loginName}>
      <StreamLinkProvider value={streamlinkStub}>
        <GlobalThemeProvider>
          <RemoteComponent {...}/>
        </GlobalThemeProvider>
      </StreamLinkProvider>
    </UserProvider>
  </Provider>,
  this.el
);
function RemoteComponent(props) {
  const { ready, failed } = useDynamicScript({
    url: props.system &amp;&amp; props.system.url,
  });

  ...

  const Component = React.lazy(
    loadComponent(props.system.scope, props.system.module)
  );

  return (
    <React.Suspense fallback="Loading Component">
      <Component />
    </React.Suspense>
  );
}

We’d taken care to use contexts where it made sense, and it was nice to see how well they integrated with remote components. The work up-front to provide useful context for our Styled Components themes and StreamLink paid off handsomely. This isn’t to say everything should go into context, more that integrating global state with remote components can be very easy.

Consider shared dependencies

Remote components still require us to provide all of the shared dependencies (for instance, React, or our component library). Modules that are intended to be remotes therefore need to use dependencies carefully.

In many ways, this isn’t much different from preparing a package for publication to npm. Designing a clean API and not pulling in unnecessary dependencies is more important, as for optimal bundling, shared dependencies need to be defined on the remote as well. It’s a rehashing of the old “do you really need Lodash, don’t use it if you can avoid it” argument. Even if a dependency is shared, it can increase the bootstrapping effort.

Design components for programmatic access

A heuristic we’ve used at Caplin when designing more complicated features is asking whether the feature might need to support a “headless” initialization. For example, what if a user were to upload a spreadsheet of data for this component, rather than entering the data manually?

We can extend this principle to remotes. So we can develop that position by asking if the entire feature can be initialised with a simple API call. As one of the integration points for remotes is access to a controller for global state, we can allow remotes to hook into the main app by simply dispatching Redux actions to activate features in the main application.

Should you use Module Federation?

Module Federation allows engineers to implement micro frontends, which enables development to scale to large and complex frontend applications. Micro frontends also enable multiple teams to build and maintain features.

Module Federation provides a critical missing piece of infrastructure to allow composing applications from features built by separate teams in larger organizations, or even totally separate organizations. The types of applications this could enable are really exciting.

If you are grappling with scaling frontends, or interested in cutting edge approaches to software composition, Module Federation is well worth investigating.

Leave a Reply

Your e-mail address will not be published. Required fields are marked *