nahuel.luca()

React Router V6 + Module Federation

Context

Recently in my work, we try implement micro-frontends with module federation (it’s no a good idea for me but, we did). So now we have a 2 applications in the same stack React + Vite + React Router. I want use the loader feature of React Router but, i found one problem, the loaders of the remote application don’t work when you expose it and use it in the host application.

Fog of War

This is a concept introduced by Remix and is based on the fog of war in video game maps, just like in video games you don't have a view of the whole map, you will discover it as you explore.

Remix send to the client one file called manifest this file contain the definitions of all our routes and other metadata, with this, Remix can create a routes tree in client-side.

📌 Manifest contains these 3 principal things:

  • Route tree: A tree of routes which defines the URLs your app can match via parent/child relationships
  • Route definition: Parts of the route used to match a URL (pathindexchildren)
  • Route implementation: Parts of the route used to load data and render the UI (loaderComponentErrorBoundary, etc.)

But, this approach has a problem, when the application is very large the manifest file increases considerably. For solve that Remix implement Eager Route Discovery this concept based on <Link prefetch> make our Remix application discover the routes based on which links are rendered.

For example if we have a route /products and in that route the view show a link that go to /products/remix-t-shirt, Eager Route Discovery fetch <ProductItem> and the product Item data.

What good is all this if at the beginning of the blog I said that my apps use react router? Well it turns out that react router also has an implementation of this called unstable_patchRoutesOnMiss .

React Router unstable_patchRoutesOnMiss

We need to define our routes and unstable_patchRoutesOnMiss, which will be called when React Router is unable to find a route. We can use the parth to match our routes and patch to patch new routes into the tree at a specific location.

const router = createBrowserRouter(
  [
    {
      path: "/",
      element: <App />,
    },
  ],
  {
    async unstable_patchRoutesOnMiss({ path, patch }) {
      if (path.startsWith("/sarasa")) {
        const routes = await getARoute();
        patch(null, routes.default.routes);
      }
    },
  }
);

How use with module federation?

Before we start we need to install @vitejs/plugin-react and @originjs/vite-plugin-federation in both applications.

Then In our remote application we have defined our routes as follows:

import { pokemonsLoader } from "./pokemons";
import { pokemonModaLoader } from "./pokemons/pokemon-modal/index";

export const routes: RouteObject[] = [
  {
    path: "/remote",
    loader: () => "hola",
    lazy: () => import("../proof-routes/remote"),
    children: [
      {
        index: true,
        lazy: () => import("../proof-routes/remote.index"),
      },
      {
        path: "nested-remote-link",
        lazy: () => import("../proof-routes/remote.nested-remote-link"),
      },
      {
        path: "pokemons",
        loader: pokemonsLoader,
        lazy: () => import("../pokemon-routes/pokemon"),
        children: [
          {
            index: true,
            path: ":pokemon-name",
            loader: pokemonModaLoader,
            lazy: () => import("../pokemon-routes/pokemon.pokemon-modal"),
          },
        ],
      },
    ],
  },
];

And define our vite.config.ts for expose our routes object:

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote",
      filename: "remoteEntry.js",
      exposes: {
        "./routes": "./src/routes/remoteRoutes",
      },
      shared: [
        "react",
        "react-dom",
        "react-router-dom",
        "react-bootstrap",
        "bootstrap",
      ],
    }),
  ],
  server: {
    port: 3001,
  },
  build: {
    target: "esnext",
  },
});

Then in our host app we will define our routes for the Router Provider:

import * as rootRoute from "./App.tsx";

const router = createBrowserRouter(
  [
    {
      ...rootRoute,
      id: "root",
      path: "/",
      element: <App />,
    },
  ],
  {
    async unstable_patchRoutesOnMiss({ path, patch }) {
      if (path.startsWith("/remote")) {
        const routes = await import("remote/routes");
        patch(null, routes.default.routes);
      }
    },
  }
);

const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

And define our vite.config.ts for connect with de remote app:

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "host",
      remotes: {
        remote: "http://localhost:3001/assets/remoteEntry.js",
      },
      exposes: {
        "./root": "./src/App",
      },
      shared: ["react", "react-dom", "react-router-dom"],
    }),
  ],
  server: {
    port: 5173,
  },
  build: {
    target: "esnext",
  },
});

The end

That's it, you have your applications working together. I hope it was helpful. I recommend reading the Remix Fog of War post to understand everything more in depth.

Resources