Micro-frontends are a strategy for splitting user interfaces up into separately deployable units. This gives teams great latitude to choose their own technologies and think in terms of self-contained programs. However, the "per page frontend" approach has disadvantages: pages cannot be shared by teams; sub-frontends are hard to modal-ise; and the user must download and execute duplicate JavaScript. Nano-frontends are an alternative that give teams similar freedoms but provide opt-in optimisations to improve user experience.
Write your frontends as libraries that take a) a DOM element to render in and b) a set of common dependencies to call (e.g. React). Then allow a scaffold app to render the entire thing and inject the dependencies. Libraries can be published by writing artefacts to S3 and then monitoring for changes using SNS/SQS.
First we need to specify a set of dependencies
import React from 'react';
import ReactDOM from 'react-dom';
import jss from 'jss';
export type Deps = {
React: typeof React,
ReactDOM: typeof ReactDOM,
jss: typeof jss
}
Then we create a higher order function that takes Deps
to return a 'concrete' frontend library function:
import { Deps } from './dependencies';
import styles from './styles';
export function library (el: Element, deps: Deps) {
const { React } = deps;
const style = jss.createStyleSheet(styles)
style.attach();
ReactDOM.render(<View />, el);
return function destroy () {
style.detach();
}
}
Now we commit the code and run a build with e.g. CircleCI. There's a testbed that can run a full browser test of our component just by injecting the necessary dependencies - including mocked dependencies. This means we can do a11y and full 'clickability' testing without having to prepare API state.
Once completed, the build process compiles the code into a bundle. Note that the dependency file only exports types, so React / ReactDOM / jss aren't included in the final bundle.
Push this to S3 and we can trigger an SNS notification (e.g. s3:ObjectCreated:Put
). This will update the 'scaffold app' that actually renders the nano-frontend.
This is a simple Node/Express/React app that routes from URLs to components that each instantiate nano-frontends. Nano-frontends are loaded via dynamic import
. For instance, on the homepage:
const { library: hero } = await import('hero-frontend');
const { library: taster } = await import('taster-frontend');
function HomePage () {
const hero = useFrontend(hero);
const taster = useFrontend(taster);
return (
<div>
<div id='hero' ref={hero} />
<div id='taster' ref={taster} />
</div>
);
}
In this case I'm using a custom hook to connect my dependency-injected frontend to my React view. You might write useFrontend
a little like this:
import { useRef, useEffect } from 'react';
const common = {
React,
ReactDOM,
jss,
mui,
useModal
};
function useFrontend (library) {
const boundEl = useRef(null);
useEffect(() => {
if (boundEl) {
return library(boundEl, common);
}
}, [boundEl]);
}
The scaffolding app then 'makes the decision' as to where nano-frontends go in terms of both URL and page location. This makes it easy for teams to discuss changes or contention, and also see the state of the frontend.
If we want teams to be independent, they need to be able to release just by updating their own repositories.
We could make this work by pushing new artefacts to S3, then using S3 events to trigger messages in SNS/SQS. In a simplified approach, a lambda could do a background re-compile of the latest frontend, which the scaffold app would then serve:
Update "hero-frontend"
-> Builds in CI
-> Executes tests
-> Compiles artefact
-> Pushes to S3 bucket frontend-artefacts/hero-frontend/111123
-> Triggers SNS topic event 'new-frontend-artefact' with pair hero-frontend:111123
-> Scaffold app now loads s3/hero-frontend.111123.js as one of its bundles
Hey there, nice idea it's pretty similar to module-federation have u seen it?