Architecture
Overview
Here is a diagram of the general architecture:
Each pink box is a GitHub repository. There are four of them:
- example tiny frontend: A simple micro frontend to be deployed and consumed
- api-cloudflare: an API using Cloudflare Workers where the latest bundle for that micro frontend is deployed
- Example host:
- example-host-remix-node: An example Remix app consuming that micro frontend
- example-host-nextjs: An example Next.js app consuming that micro frontend
- tiny-client: A small library to consume the micro frontend at runtime
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 mode. 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 SSRloadExampleTinyFrontendClient
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 Workers and KV.
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
}
2
3
4
/tiny/bundle/{bundleName}
should respond with the source of the bundle withbundleName
.
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 app
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> {
// ...
};
2
3
4
5
6
7
8
9
When you call this method it uses the Cloudflare API 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 Workers (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
orundefined
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 thanX
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 to0
).retryPolicy.delayInMs
delay between each retries in milliseconds. The delay will be doubled between each tried. For example for a delay of100ms
and3 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:
- A Remix app deployed on Fly.io
- A Next.js app deployed on Vercel
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 useEffect
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 frontend by calling loadExampleTinyFrontendServer
before rendering happens.
We then use the server side loaded component in our Remix route.
In root.ts
while on server we add a __TINY_FRONTEND_SSR__
label. (It's a similar approach taken for Styled Components with Remix.)
Finally, once we rendered, we replace __TINY_FRONTEND_SSR__
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 frontend by calling loadExampleTinyFrontendClient
before rehydration.
We then use the client side loaded component in our Remix route.
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 useEffect
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 frontend by calling loadExampleTinyFrontendServer
before rendering happens. We also inject the JS and CSS script tags for the Tiny Frontend in the document head.
We then use the server side loaded component in our Next.js page.
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 component that can load itself on the client and suppresses hydration warnings using the withHydrationSuppress
HOC. This uses the same technique as react-lazy-hydration.
We then use the client side component in our Next.js page 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 SSR 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 SSR 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 runtime. This means this approach won't work in that environment for SSR ๐ข .