Infoxicator.com

Holocron Module Composition

Published
cover image

Version en Español

In my previous article, I introduced Holocron, a new way of creating Micro Frontends in React. Now, here is the deal…

“How do you assemble all the jigsaw pieces in a Micro Frontend architecture puzzle? the answer is… NOT using Iframes!”

Most of the examples that you find on the internet on how to create a Micro frontend based application mention the usage of iframes, but let’s be honest, Iframes are bad 🙁… And they are by no means the elegant solution that the modern web is used to these days… so my question again is:

Is there a secure way to assemble dozens of React modules on a single page, preserving a small page load, Server Side Rendering and loading just parts of the frontend experience as they are required?

With One App and Holocron, the answer is YES!… all of the above is possible and much more!

Module Composition with Holocron

Holocron modules are loaded in memory and get updated dynamically whenever The One App Server polls the module map that contains the URLs to the Module bundles (e.g. my-module.browser.js). When a new HTTP request arrives, the One App Server renders one or more modules on the page becoming a single experience to the user. Similar to how components in React work, modules are composable, meaning that a single module can load and reuse other modules.

The Root Module

The first or “top-level” module is called The root module. This is module is the entry point for your application. You can compare it to the top-level component of a React application where all the other components are rendered as children underneath. As well as rendering the children modules, the root module is in charge of the App configuration, like the Content Security Policy, CORS origins, and the Routing.

Children Modules

These modules are the true definition of what micro frontend is in practice. They are self-contained Web Experiences that consist of React Components, Redux Reducers, and Actions as well as Routes. They are independent of other modules, load all the data / make all the API requests they need to render, and can be reused across the application with a configurable API via props.


Let’s Get Coding!

We are going to use the root module that we created in the previous post. If you don’t have one yet, don’t worry just run the generator twice, once for your root module, and once for your child module; then select the correct module type when prompted.

Use the following command to create a boilerplate module using the One App Module Generator:

export NODE_ENV=development
npx -p yo -p @americanexpress/generator-one-app-module -- yo @americanexpress/one-app-module

Follow the steps, give your module the name “child”. Head over to your favorite code editor and open the package.json file of your root module and add the relative path to your child module to the modules array under the one-amex section.

{
  "name": "root-module",
  "version": "1.0.0",
  "description": "",
  "contributors": [],
  "scripts": {
    "start": "one-app-runner",
    "prebuild": "npm run clean",
    "build": "bundle-module",
    "watch:build": "npm run build -- --watch",
    "clean": "rimraf build",
    "prepare": "npm run build",
    "test:lint": "eslint --ignore-path .gitignore --ext js,jsx,snap .",
    "test:unit": "jest",
    "test": "npm run test:lint && npm run test:unit"
  },
  "one-amex": {
    "runner": {
      "dockerImage": "oneamex/one-app-dev:latest",
      "rootModuleName": "root-module",
      "modules": [
        ".",
        "../child"
      ]
    }
  },
  "dependencies": {
    "@americanexpress/one-app-router": "^1.0.0",
    "holocron": "^1.1.0",
    "react": "^16.12.0",
    "content-security-policy-builder": "^2.1.0"
  },
  "devDependencies": {
    "@americanexpress/one-app-bundler": "^6.0.0",
    "@americanexpress/one-app-runner": "^6.0.0",
    "amex-jest-preset-react": "^6.0.0",
    "babel-eslint": "^8.2.6",
    "babel-preset-amex": "^3.2.0",
    "enzyme": "^3.11.0",
    "enzyme-to-json": "^3.4.4",
    "eslint": "^6.8.0",
    "eslint-config-amex": "^11.1.0",
    "jest": "^25.1.0",
    "rimraf": "^3.0.0"
  },
  "jest": {
    "preset": "amex-jest-preset-react"
  }
}

The modules array points to the relative path of other modules in the same project that can be loaded during local development. Add to this array the relative paths of any modules that you wish to load locally.

Composing Modules Using Routes:

The first way of composing Holocron modules is by creating a route that will load the individual module using holocron-module-route.

npm i -S holocron-module-route

Let’s create a new route called child-route which will load our child module.

In your root module’s childRoutes.jsx file, add the following:

import React from 'react';
import { Route } from '@americanexpress/one-app-router';
import ModuleRoute from 'holocron-module-route';


const childRoutes = () => [
  <Route path="/" />,
  <ModuleRoute path="child-route" moduleName="child" />,
];

export default childRoutes;

Next, we need to render the child module in the Root module’s main component. Modules loaded as routes using ModuleRoute will be passed to the root module’s render method via props.

import React from 'react';
import childRoutes from '../childRoutes';

const RootModule = ({ children }) => (
  <div>
    <h1>Welcome to One App!</h1>
    { children }
  </div>
);

RootModule.childRoutes = childRoutes;

if (!global.BROWSER) {
  // eslint-disable-next-line global-require
  RootModule.appConfig = require('../appConfig').default;
}

export default RootModule;

After you have made your changes, bundle your root module by running the npm run build command. You can also use the npm run watch:build command to listen for changes during development.

To see the results, start the One App Server by running the npm start command (requires Docker) from the root module and head over to http://localhost:3000/child-route. You should see the contents of your root module as well as the contents of your child module.

Note: The first time that you use npm start, it might take a couple of minutes to download the One App Development Image from Docker hub.

Challenge Time!: Make changes to the render method of your child module, keep the One App server running in the background and don’t forget to use npm run build on your module to bundle the changes.


Holocron ComposeModules

The second method is to load modules as “chunks” within the same page using composeModules. This method is useful when you want to render multiple modules on the same route.

In the following example, we are going to create a third module, but in this case, instead of loading it on a route using holocron-module-route, we will let the child module compose it and load it in its own render method.

Let’s create a new module called grand-child using the generator. Select “child as the module type.

*The recommended folder structure is to keep related modules under the same folder so it is easier to add them by the relative path.

holocron-example
├── root-module
├── child
├── grand-child

Add the grand-child to the modules array in package.json of the root-module so it is loaded during development:

 "one-amex": {
    "runner": {
      "dockerImage": "oneamex/one-app-dev:latest",
      "rootModuleName": "root-module",
      "modules": [
        ".",
        "../child",
        "../grand-child"
      ]
    }
  }

The first step is to dispatch composeModules from the module that you want to load the grand-child module from. composeModules is an action creator that loads Holocron modules and their data.

Open the main component of the child module Child.jsx, so we can load the grand-child module that we just created.

import React from 'react';
import { composeModules } from 'holocron';

export const Child = () => (
  <div>
    <h1>I am the child module!</h1>
  </div>
);

export const loadModuleData = ({ store: { dispatch } }) => dispatch(composeModules([
  { name: 'grand-child' },
]));

Child.holocron = {
  name: 'child',
  loadModuleData,
};

export default Child;

We need to dispatch the composeModules action creator from the loadModuleData function. This Holocron function fetches the data required by your module. Lastly, we add the loadModuleData function to the Holocron module configuration object.

Holocron RenderModule

The last step is to render the grand-child module in our child module by using the RenderModule React component.

Inside the render method of your Child.jsx module, add the following:

<RenderModule moduleName="grand-child" />

The final version of Child.jsx should look like this:

import React from 'react';
import { composeModules, RenderModule } from 'holocron';

export const Child = () => (
  <div>
    <h1>I am the child module!</h1>
    <RenderModule moduleName="grand-child" />
  </div>
);

export const loadModuleData = ({ store: { dispatch } }) => dispatch(composeModules([
  { name: 'grand-child' },
]));

Child.holocron = {
  name: 'child',
  loadModuleData,
};

export default Child;

To view the final result, bundle your child module by running npm run build and check your changes on http://localhost:3000/child-route

Ensure your One App Server is still running in the background, if not run npm start again from your route-module.

You should see your three modules rendering on the page:

Challenge Time! (x2): Add 2 more modules, 1 loaded under the /child-route/module-4 and the last module composed and rendered by the 4th module.

Horray! 🎉

Conclusion

The powerful composition model that makes React so flexible can now be extended to whole frontend experiences encapsulated in Holocron modules. These experiences can be developed, tested, and deployed individually without the need for a server restart. They can also be shared across the application the same way components are shared and reused, reducing code duplication and allowing independent teams to work concurrently on different parts of the application without breaking or interfering with each other’s work.

The source code for this article can be found here: https://github.com/infoxicator/holocron-composition-example


Recent Posts

Is React going anywhere?

Earlier this year I had an interesting conversation with a CTO of a price comparison website (e-commerce) and he mentioned that they are moving away from React. “Wait, what?”… was my Reaction (pun 👊 intended)… please tell me more! “Yeah, it is not working for us, we are moving away for performance reasons, e-commerce is[…]

React Router 6 Deferred Fetch

React Router 6 introduced the “deferred” API that allows you to “await” for critical data and “defer” optional data when calling your loaders.

React Router 6.4 Code-Splitting

Single Page Applications that are unable to migrate can benefit from all of the goodies 🎁 that Remix provides.