Skip to content

Instantly share code, notes, and snippets.

@jbreckmckye
Last active September 16, 2024 13:57
Show Gist options
  • Save jbreckmckye/aa8c6cee2f2ac938bc2fc1c6c9127647 to your computer and use it in GitHub Desktop.
Save jbreckmckye/aa8c6cee2f2ac938bc2fc1c6c9127647 to your computer and use it in GitHub Desktop.
Nano-frontends

Nano-Frontends

Abstract

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.

Give me the gist

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.

What does a 'dependency injected' frontend look like?

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.

The scaffold app

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.

Publishing workflow

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
@luistak
Copy link

luistak commented Sep 16, 2024

Hey there, nice idea it's pretty similar to module-federation have u seen it?

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