Architecture

Overview

Here is a diagram of the general architecture:

General architecture diagram

Each pink box is a GitHub repository. There are four of them:

Example Tiny Frontend

This is the code of the micro frontend you want to use in a host app. It has two folders:

  • app
  • contract

app

The app is the source of the component you want to expose. In this case it exposes a component called ExampleTinyFrontend.

It's bundled as a JavaScript UMD bundle, and a CSS bundle using Vite library modeopen in new window. React is marked as external for the build. It will be provided by the host.

Both bundles are then deployed to the Api Cloudflare to be consumed by the host.

contract

The contract is a regular npm package. It contains:

  • The TypeScript type for ExampleTinyFrontend
  • Two functions to consume the latest deployed app at runtime:
    • loadExampleTinyFrontendServer to be used on SSR
    • loadExampleTinyFrontendClient to be used client side
  • Peer dependencies (in this case React)

This package will pull the latest deployed app bundle at runtime from the api.

Api Cloudflare

This is an example implementation of a tiny frontend api using Cloudflare Workersopen in new window and KVopen in new window.

The repository has two folders:

  • api
  • deploy

api

A tiny frontend api must have the two following endpoints:

  • /tiny/latest/{tinyFrontendName}/{contractVersion} should respond with:
{
  "umdBundle": "tinyFrontend-umd-bundle-name.js",
  "cssBundle": "tinyFrontend-css-bundle-name.css" // Optional
}
1
2
3
4
  • /tiny/bundle/{bundleName} should respond with the source of the bundle with bundleName.

In the case of this Cloudflare implementation, we use KV to store the source of the latest bundles and serves them at runtime.

TIP

You could write this API with whatever stack you want. As long as you have the correct endpoints, and a way of deploying bundles to it, it would work.

deploy

A simple npm package that can be used to deploy a bundle to api. It's used by the Example Tiny Frontend appopen in new window to deploy its bundle.

It exposes the following function:

async function deployBundle({
  cloudflare, // Cloudflare credentials
  name, // Name of the tiny frontend to deploy
  contractVersion, // Version of the contract to deploy for
  umdBundlePath, // Path of the JS UMD bundle on disk
  cssBundlePath, // Path of the CSS bundle on disk
}): Promise<void> {
  // ...
};
1
2
3
4
5
6
7
8
9

When you call this method it uses the Cloudflare APIopen in new window to push the JS UMD and CSS bundles source to KV.

Tiny Client

This is where the magic happens โœจ ! This package can fetch and evaluate a tiny frontend at runtime either client or server side.

The main steps it always follows are:

  • Call a tiny-api to fetch the name of the latest UMD and CSS bundles.
  • Fetch the latest UMD JS bundle source.
  • Evaluate the UMD bundle source while providing dependencies to it.
  • Return a reference to the component(s) exported by the UMD JS bundle.

Client side only

The main steps above will happen first. Then, if there is a CSS bundle, it will create a link to that CSS file.

Server side (SSR) with client side rehydration

The server side loading function executes the main steps above.

It also returns a piece of HTML to be added to the SSR result. This piece of HTML will:

  • Add a link preload to the UMD JS bundle.
  • Add a link to the CSS bundle.
  • Expose the config fetched from the tiny-api on window to make sure both client and server will use the same bundle.

Once on the client side, before calling ReactDOM.hydrate, you need to call loadTinyFrontendClient. This will load and evaluate the UMD bundle based on the config exposed on window.

Then, when React will rehydrate the page, your tiny frontend will already be loaded and the component will get rehydrated โœจ !

WARNING

Loading a tiny frontend on server side requires an environment that support runtime JS source evaluation (new Function(source)).

This is currently not supported on Cloudflare Workersopen in new window (for security reason), therefore this won't work if you deploy your host apps to that platform.

You can however still use the client side only version in that case.

Cache and Retry Policy

Both loadTinyFrontendClient and loadTinyFrontendServer accept some LoadingOptions.

cacheTtlInMs

By default, tiny frontend will cache the latest fetched bundle in memory for 2 minutes. This means that for 2 minutes, all calls to loadTinyFrontendClient or loadTinyFrontendServer will return the same bundle even if there is a newer bundle deployed.

There is a compromise to be had between speed of response, and speed of reflecting changes when new bundle are deployed.

If you wish to change that behaviour you can pass the cacheTtlInMs loading option to alter it:

  • Passing null or undefined will cache for as long as your server is running and never fetch again.
  • Passing 0 will disable the cache (we will fetch the latest configuration for each request)
  • Passing any other number X will result in reusing the bundle in memory if it has been fetched less than X milliseconds ago.

retryPolicy

By default, tiny frontend won't retry if there is an error fetching either the name of the latest bundle from the API or its content, and just throw.

If you want tiny frontend to automatically retry in case of errors, you can pass retryPolicy to the loading options:

  • retryPolicy.maxRetries maximum number of retries after the first failure (defaults to 0).
  • retryPolicy.delayInMs delay between each retries in milliseconds. The delay will be doubled between each tried. For example for a delay of 100ms and 3 retries, it will do:
    • Try once, fails. Wait 100ms.
    • First retry, fails. Wait 200ms
    • Second retry, fails. Wait 400ms
    • Third retry, fails. Give up and throw error.

Example Hosts

There are two example hosts:

They both have the Example Tiny Frontend contract as an npm dependency.

As everything is better explained with a meme:

Remix

Client side only

To consume the example tiny frontend on client side, we simply load it in a useEffectopen in new window by calling loadExampleTinyFrontendClient and passing the URL of our tiny-api.

While the tiny frontend is loading, we show a Loading... label.

When the tiny frontend is loaded, we set the component we receive in a useState. We then simply renders it like any other React component ๐Ÿฐ .

Server side (SSR) with client side rehydration

This is a bit more involved, but still rather straightforward.

On the server

In entry.server.ts we load the tiny frontendopen in new window by calling loadExampleTinyFrontendServer before rendering happens.

We then use the server side loaded component in our Remix routeopen in new window.

In root.ts while on server we add a __TINY_FRONTEND_SSR__open in new window label. (It's a similar approach taken for Styled Components with Remixopen in new window.)

Finally, once we rendered, we replace __TINY_FRONTEND_SSR__open in new window in the output HTML string with whatever was returned by loadExampleTinyFrontendServer.

Our Remix app is now loading that tiny frontend on the server and rendering it โœจ! Let's see what we need to add to be able to rehydrate.

On the client

In entry.client.ts we load the tiny frontendopen in new window by calling loadExampleTinyFrontendClient before rehydration.

We then use the client side loaded component in our Remix routeopen in new window.

And... That's it, we now have fully working, independently deployed frontend component, with SSR and rehydration ๐Ÿ˜ฑ !

Next.js

Client side only

To consume the example tiny frontend on client side, we simply load it in a useEffectopen in new window by calling loadExampleTinyFrontendClient and passing the URL of our tiny-api.

While the tiny frontend is loading, we show a Loading... label.

When the tiny frontend is loaded, we set the component we receive in a useState. We then simply renders it like any other React component ๐Ÿฐ .

Server side (SSR) with client side rehydration

This is a bit more involved, but still rather straightforward.

On the server

In _document.ts we load the tiny frontendopen in new window by calling loadExampleTinyFrontendServer before rendering happens. We also inject the JS and CSS script tags for the Tiny Frontend in the document headopen in new window.

We then use the server side loaded component in our Next.js pageopen in new window.

Our Next.js app is now loading that tiny frontend on the server and rendering it โœจ! Let's see what we need to add to be able to rehydrate.

On the client

We create a client side version of our componentopen in new window that can load itself on the client and suppresses hydration warnings using the withHydrationSuppress HOCopen in new window. This uses the same technique as react-lazy-hydrationopen in new window.

We then use the client side component in our Next.js pageopen in new window when the Server side component is undefined.

And... That's it, we now have fully working, independently deployed frontend component, with SSR and rehydration ๐Ÿ˜ฑ !

Current known limitations

TIP

Below is a list of limitations I'm currently aware of with the example hosts. Contributions are welcome to help solve them, or to add new limitations to the list ๐Ÿ˜‰ .

๐Ÿ”ย ย The tiny frontend is loaded on every request

This one is easily overcome. You could implement a simple server side cache, and refresh the tiny frontend in the background every X minutes for example. We just haven't done it in this example.

๐Ÿ—‘ย ย The tiny frontend is loaded for every route, no matter if it used or not

This could potentially be fixed with a more involved implementation. We could for example imagine "collecting" rendered components on server on a route, and only loading those before client rehydration.

However, that solution probably would require some new hooks in Remix or Next.js, for example async client side route transitions guards.

TIP

This could also be solved using Suspense for data fetching on SSRopen in new window in React 18, as a tiny frontend could be loaded as part of a Suspense boundary when a given route is rendered.

๐Ÿ’ฟย ย The tiny frontend can't load its own data before SSR

The tiny frontend might be able to provide some kind of data loader, but again this might require some hooks in Remix or Next.js to call them at the right time.

TIP

Once again Suspense for data fetching on SSRopen in new window in React 18 could save us here, as a tiny frontend could use its own Suspense boundary to load its data when it's rendered.

๐Ÿšย ย The tiny frontend can't declare its own routes

As stated in the: what tiny frontend isn't section, this is not something this approach is trying to solve at the moment.

โ›…๏ธย ย SSR doesn't work in Cloudflare Workers

Sadly, Clouflare Workers doesn't provide any way of loading JS dynamically at runtimeopen in new window. This means this approach won't work in that environment for SSR ๐Ÿ˜ข .