Pull to refresh

Microfrontend. Server fragments — frontend as it supposed to be

Level of difficultyMedium
Reading time7 min
Views2.3K

When you think about the frontend - how should it look like in the ideal world? I see it as:

  1. Single Page Application (SPA) - user don't wait when you load the whole set of assets again when you go to the next route - best UX

  2. Server-side rendering (SSR) - user receives the page content faster since it's rendered on server and some of the content is cached on CDN - best SEO and great performance

  3. Independent deployments by different teams - factor that is usually skipped and underestimated but it gives you as a developer the confidence you don't break the whole site once your deployment is failed or if it has bugs, you also don't wait when other team member's build is finished and nobody accidentally touches your area of responsibility - great DX, no regression testing is required

What does market provide today?

  • ⭐️ ⭐️ ❌ SPA + SSR but no independent deployments

  • ⭐️ ❌ ⭐️ SPA without SSR but with independent deployments

  • ❌ ⭐️ ⭐️ not a SPA but SSR + independent deployments

In this article i'll describe a way how to have all three on the example of the react application.


⭐️ ⭐️ ⭐️ SPA + SSR + Independent deployments by different teams

Working demo is here: https://github.com/egorvoronov/react-microfrontend

First of all, let's imagine we develop a great site and we have 2 teams, let's split them and ask Team A to focus on a Header component where Team B could focus on a Footer component. We also would like to have all the components to be rendered together when our user visits our site. Let's create 3 different folders for the 3 use-cases above:

  1. appRenderer - where we glue our fragments

  2. fragmentHeader - where Team A develops a great header

  3. fragmentFooter - where Team B develops a great footer

Let's consider appRenderer

It's just a simple application that have 2 build points, one for server and one for client using webpack.

For client's build we'll add ModuleFederationPlugin to make sure that we're able to rehydrate the rendered on a server content once the page is in the user's browser:

        new ModuleFederationPlugin({
            name: 'appRenderer',
            filename: "remoteEntry.js",
            exposes: {
                './AppRendererContextForFragments': './context/AppRendererContextForFragments',
            },
            remotes: {
                fragmentHeader: `fragmentHeader@${getRemoteEntryUrl(3001)}`,
                fragmentFooter: `fragmentFooter@${getRemoteEntryUrl(3002)}`,
            },
            shared: [
                {
                    react: { singleton: true, requiredVersion: deps.react },
                    'react-dom': { singleton: true, requiredVersion: deps[ 'react-dom' ] }
                },
                // https://github.com/module-federation/module-federation-examples/blob/f196c14fb91476152e0c3029e79a1758c7399ffb/shared-routing/shell/webpack.config.js#L77
                './context/AppRendererContextForFragments'
            ],
        })

That would allow us to use the header fragment in our application like below:

// ...
import FragmentHeader from 'fragmentHeader/Fragment';
// ...

const Content = ({ state }) => {
    return (
        <AppRendererContextForFragments.Provider value={{
            userAgent: state.userAgent,
        }}>
            <FragmentHeader firstname={state.firstname} />
            <div style={{height: "300px", backgroundColor: "grey", textAlign: "center", paddingTop: "100px"}}>Great Body of the page (Another fragment)</div>
            <FragmentFooter />
        </AppRendererContextForFragments.Provider>
    );
}

I'm using context there to pass userAgent also to make sure that server is able to render mobile version of the header and client is able to rehydrate the same, but that's not an important part here.

From server side we also need to know how to handle that import FragmentHeader from 'fragmentHeader/Fragment';

❗️And the main challenge goes here, we'll touch this topic again below in the article but here is the short story. Default webpack module federation feature gives us ability to use fragments on server BUT such files that consists of fragments are supposed to be in the same filesystem as appRenderer file with server code - which is not great since it's not clear how to deploy the new version of the header independently without affecting the main application. There are some options, for example, we could use shared volume on our cluster and just replace the files there but appRenderer server is already running and all the variables inside that server are already initialised, so, without the rude hacks it's not possible to replace header component on a server without a restart or redeploy of the main application (appRenderer server) - so it's not an independent deployment anymore.

So, we need another solution. Ideally, it would be great if appRenderer is a standalone application but once it sees import FragmentHeader from 'fragmentHeader/Fragment'; it just sends the request to some other server and gets the html back and just embeds that html to the resulting response that would be sent to the user to his browser.

To do that, here is the example of plugin that I wrote. It's really naive, but essentially gives us what we want, once it sees import FragmentHeader from 'fragmentHeader/Fragment'; - it just makes the http request to fetch the server html for it. Let's add that plugins (called ReactMFRemoteFragmentPlugin) to the server build:

plugins: [
        new ModuleFederationPlugin({
            name: "appRenderer",
            library: { type: "commonjs-module" },
            filename: 'remoteContainer.js',
            exposes: {
                './AppRendererContextForFragments': './context/AppRendererContextForFragments',
            },
            remotes: {
                fragmentHeader: path.resolve(
                    __dirname,
                    "./dist/fragmentHeader/remoteContainer.js"
                ),
                fragmentFooter: path.resolve(
                    __dirname,
                    "./dist/fragmentFooter/remoteContainer.js"
                )
            },
            shared: [
                // { react: { singleton: true }, "react-dom": { singleton: true } }
                './context/AppRendererContextForFragments'
            ],
        }),
        new webpack.DefinePlugin({
            'process.env.REACT_APP_IS_SERVER': JSON.stringify(process.env.REACT_APP_IS_SERVER),
        }),
        new ReactMFRemoteFragmentPlugin({
            name: 'fragmentHeader',
            url: 'http://localhost:3001'
        }),
        new ReactMFRemoteFragmentPlugin({
            name: 'fragmentFooter',
            url: 'http://localhost:3002'
        }),
    ],

As you see, at the bottom we add ReactMFRemoteFragmentPlugin specifying urls from which we'll derive the content for the fragments. Again, plugin itself is really dummy but works which is exactly what we want for our POC. You could also find the source code of that plugin in npm and in the repo if you're interested in the details. We'll also slightly cover it below in the article as well.

That's all what we need for appRenderer. Let's switch to fragments.

Considering fragmentHeader, here are the main points you need to know:

  1. we use the empty client/index.js file there - since we don't need a standalone build for the header for now, we're interested only in the fragment output that webpack ModuleFederation plugin would generate for us.

  2. we use the default ModuleFederationPlugin there, nothing fancy for the client's build, such files will be used for the rehydration process only in the user's browser after content is delivered by appRenderer server:

        new ModuleFederationPlugin({
            name: 'fragmentHeader',
            library: { type: 'var', name: 'fragmentHeader' },
            remotes: {
                appRenderer: `appRenderer@${getRemoteEntryUrl(3000)}`,
            },
            filename: 'remoteEntry.js',
            exposes: {
                './Fragment': './src/Fragment',
            },
            shared: {
                react: { singleton: true, requiredVersion: deps.react },
                'react-dom': { singleton: true, requiredVersion: deps['react-dom'] }
            },
        }),
  1. and the last important point here, that we don't use ModuleFederationPlugin for the server's build in fragments since we won't be using standard server support of ModuleFederationPlugin because of the reason named above - it does not support independent deployments of the fragments. When we talk about fragments server code we're only interested in html code generated by fragment's server code using the Header fragment that we'll just embed to the appRenderer server's html response later. And to make it happened, we just need to write the following naive server for fragments using express:

// ...
const app = express();

app.get('*', (req, res, next) => {
    setTimeout(() => {
        res.send({
            htmlFragment: ReactDOMServer.renderToString(<Fragment {...req.query}/>),
            link,
        });
    }, 5000);
})

// ...

I'm adding 5 seconds delay here just to simulate the async nature of the requests. All the fetching and embedding magic could be found in the modules folder (webpack-plugin is published to npm as react-mf-remote-fragment).

2 words on the magic 🪄 there. Once server sees import FragmentHeader from 'fragmentHeader/Fragment'; in appRenderer server code it just uses the default module federation server feature looking for the server version of header fragment in filesystem but the issue, as highlighted above, is that we can't use header code there since that would be the same filesystem and our inability to independently deploy header later since we won't be able to replace the header code without app restart or rude hacks. But we, instead, could use the plugin that will generate a file with the component, essentially just a wrapper, and the responsibility of that component would be just to derive the exact header code & components from the proper url. So, frankly speaking, we, actually, use the default ModuleFederation server support that looks for the header fragment in the filesystem but we use it not for delivering the exact header code but for delivery of the wrapper fragment - code that would know from where to fetch that header code from. Such file is being generated during the build phase and since the responsibility of that file is not changed time over time we could generate it once and just update the fragment's server that delivers the proper html now.

🙋🏻‍♀️ All good so far, but how our server rendering phase would stop and wait once we go to the other server for header's html content? React usual standard methods like renderToString & renderToStaticMarkup do not support it and is considered as one-go operation like rendered and then sent to the browser.

🤯 Standard methods don't support it but we'll use the new methods that react v18 brought to us. Essentially, that wrapper code that was generated is just a react component that uses new react v18 feature of React.Suspense. React.Suspense started supporting server-side rendering and that Suspense is being resolved once the call to other server is done and html is translated back to react component that is rendered again to html on appRenderer during the content is served back to the user's browser. If you're interested in the details, just look to the modules folder, all magic is in these 2 files:

And as a final step, to prevent the react to emit us an error that server rendered content does not differ from the client render, we need to return the same content as it was rendered by a server. To make it happened, there is another helper react component container was created that wraps a Header component for client only. On a server we need the clean component that we just render to html and return back but on a client we need a clean component plus a wrapper that adds the missing tags/parts that are not part of the clean component but was returned back from the server. Here is how we use it in our main entry file Fragment.js on every fragment:

function Fragment() {
    if (process.env.REACT_APP_IS_SERVER === '1') {
        return <Header/>;
    } else {
        return <FragmentClientContainer name="fragmentHeader">
            <Header />
        </FragmentClientContainer>;
    }
}

FragmentClientContainer just makes sure that code is the same for server and client and just waits the initialisation of the fragment which could be useful once you start using lazy loadable components inside one or another fragment, for example.

And here is the demo (you could run it locally, just clone it from https://github.com/egorvoronov/react-microfrontend):

Better video quality is on rutube

Let me know if you need more details, and you could contact me via: egorvoronov@gmail.com

Thanks and have a good day.

Tags:
Hubs:
Rating0
Comments1

Articles