DEV Community

Cover image for Micro-frontend with Module Federations [Part 1] - Vite + React
Kevin Toshihiro Uehara
Kevin Toshihiro Uehara

Posted on • Edited on

Micro-frontend with Module Federations [Part 1] - Vite + React

Hi folks! It's a pleasure to have you here again!

Michael in the office celebrating

In this article, which I'm thinking of breaking in two, I'll be talking about Micro-frontends using Module Federations.
This first article I will be using Vite and on the part 2, I will be using Create React App (CRA).

But what we will create? (spoilers)

Gif of image pokemons list

The list of pokemons is a Micro-Frontend (MF), where I will expose the component and the store (pokemon selected). The main page will consume the MF and display the pokemon selected, provided by MF state (using Jotai).

Summary:

Introduction

First, we need to understand what is a MF? And why we you can use this approach. The MF concept was create when we have multiple teams and we need to separate our application components between them.

Each team is responsible to maintain the MF, and can be a component or a MF page route.

Image that exists a team where is responsible to maintain the Home Page and other to maintain the Cart component or Page. So we can scale the application and make it smaller, BUT whe have some trade-off that we will address later.

Creating an MF not so long ago was difficult to create and maintain. But currently creating has become something easy, but maintaining it will depend on the team.

So Webpack 5 introduce the new concept of share components. The Module Federations.

Module Federations

Module Federations image with webpack

Module Federation is a specific method of enabling what are commonly called “micro frontends” in JavaScript applications using Webpack 5.

According the Webpack documentation:

Multiple separate builds should form a single application. These separate builds act like containers and can expose and consume code between builds, creating a single, unified application.
This is often known as Micro-Frontends, but is not limited to that.

Just to remember, the module federations is only available on the version 5 of webpack.

With Module Federations we can share not just components, but states as I mentioned above, using Jotai.

Show Me The Code

So let's create our application to see how module federations works on Vite. We will have two webapps created using vite, first the pokemons-list that will expose the component and state. And the second pokemons-home that will consume the MF and allow to select and display the pokémon.

First, let's create our directory using:



mkdir vite && cd vite


Enter fullscreen mode Exit fullscreen mode

And now, we will create our MF using:



yarn create vite pokemons-list --template react-ts


Enter fullscreen mode Exit fullscreen mode

Install the packages on the project created, just using:



yarn


Enter fullscreen mode Exit fullscreen mode

Let's add the jotai as dependency, using:



yarn add jotai


Enter fullscreen mode Exit fullscreen mode

Now we will use a plugin made available by vite, called originjs/vite-plugin-federation. So let's install as a dev dependency using:



yarn add -D @originjs/vite-plugin-federation


Enter fullscreen mode Exit fullscreen mode

Now Let's code!

First on src folder, I will be creating some foldes called types, components and atoms.

  • Types will have only the type of Pokemon definition
  • Components will have only one component, that will be de List of pokémons
  • Atoms will have the state of our application

Folders of project image

So on src/types, let's create the Pokemon.ts



export interface IPokemon {
  id: number;
  name: string;
  sprite: string;
}


Enter fullscreen mode Exit fullscreen mode

On the src/atoms, let's create our state of pokémons, also I called Pokemon.ts.
I will be using Jotai, so I will not delve into the subject, as there is an article where I specifically talk about this state manager.



import { atom, useAtom } from "jotai";
import { IPokemon } from "../types/Pokemon";

type SelectPokemon = IPokemon | undefined;

export const pokemons = atom<IPokemon[]>([]);
export const addAllPokemons = atom(
  null,
  (_, set, fetchedPokemons: IPokemon[]) => {
    set(pokemons, fetchedPokemons);
  }
);
export const selectPokemon = atom<SelectPokemon>(undefined);

const useSelectPokemon = () => useAtom(selectPokemon);
export default useSelectPokemon;


Enter fullscreen mode Exit fullscreen mode

And in our components src/components/PokemonList, let's create two files. First the PokemonList.module.css.
Let's just use CSS modules to our styles.



.container {
    & > h1 {
    color:#1e3a8a;
    font-size: 25px;
    };

    display: flex;
    flex-direction: column;
    border: 3px solid #1d4ed8;
    width: fit-content;
    padding: 5px 5px;
}
.pokemonCardContainer {
    display: flex;
}

.pokemonCard {
    font-family: Arial, Helvetica, sans-serif;
    color: #fff;
    background-color: #1e3a8a;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    margin: 4px;
    padding: 5px;
    border-radius: 4px;
}

.pokemonCard:hover {
    cursor: pointer;
    background-color: #1d4ed8;
}


Enter fullscreen mode Exit fullscreen mode

And I will create the src/components/PokemonList the index.tsx that will be our MF:



import { useEffect } from "react";
import useSelectPokemon, {
  addAllPokemons,
  pokemons as pokemonState,
} from "../../atoms/Pokemon";
import { useAtom } from "jotai";

import style from "./PokemonList.module.css";

const PokemonList = () => {
  const [, addPokemons] = useAtom(addAllPokemons);
  const [pokemons] = useAtom(pokemonState);
  const [, setSelectPokemon] = useSelectPokemon();

  const fetchPokemons = async () => {
    const response = await fetch(
      "https://raw.githubusercontent.com/kevinuehara/microfrontends/main/mocks/pokemonList.json"
    );
    const jsonData = await response.json();
    addPokemons(jsonData);
  };

  useEffect(() => {
    fetchPokemons();
  }, []);

  return (
    <div className={style.container}>
      <h1>Pokémon List Micro Frontend</h1>
      <div className={style.pokemonCardContainer}>
        {pokemons.map((pokemon) => {
          return (
            <div
              className={style.pokemonCard}
              key={pokemon.id}
              onClick={() => setSelectPokemon(pokemon)}
            >
              <img
                src={pokemon.sprite}
                aria-label={`Image of pokemon ${pokemon.name}`}
              />
              <label>{pokemon.name}</label>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default PokemonList;


Enter fullscreen mode Exit fullscreen mode

You can clean the App.tsx, remove the App.css and clean the index.css.

On your App.tsx you can just call your component:



import PokemonList from "./components/PokemonList";

function App() {
  return (
    <>
      <PokemonList />
    </>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Don't worry about the fetch URL, because I'm exposed on my repository to bring 5 pokémons.

Now we are prepared to config our MF. So in the file vite.config.ts, we are going to import the @originjs/vite-plugin-federation and expose what we need. The PokemonList and Jotai State. Let's change to:



import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "pokemonList",
      filename: "remoteEntry.js",
      exposes: {
        "./PokemonList": "./src/components/PokemonList",
        "./Pokemon": "./src/atoms/Pokemon.ts",
      },
      shared: ["react", "react-dom", "jotai"],
    }),
  ],
  build: {
    modulePreload: false,
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
});


Enter fullscreen mode Exit fullscreen mode

I'm importing as default the federation of the plugin, and we need to provide some properties:

  • name: the name of our object of module federation

  • filename: This is very important, because the build of the app will generate a single file that will be our manifest to expose the componets. (I recommended to use remoteEntry.js as default)

  • filename: This is very important, because the build of the app will generate a single file that will be our manifest to expose the componets.

  • exposes: The object where we will let's say what we're going to expose. In the example the atom of jotai and the PokemonList component.

  • shared: It's important because when we have other applications running our MF, we need to provide what is needed to render the MF. In this case, react, react-dom and jotai.
    And even if the other application that consumes it is in react, the module federations plugin will define the import and if you already have it, it will not reimport.

It's very important when we have a MF that the runtime sharing it's been running on some port. So, let's fix the port on our package.json:



 "scripts": {
    "dev": "vite --port 5173 --strictPort",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview --port 5173 --strictPort"
  },


Enter fullscreen mode Exit fullscreen mode

Now, you can try run the project. But you need to run the build, to generate the remoteEntry.js and using the preview mode. So just run using:



yarn build && yarn preview


Enter fullscreen mode Exit fullscreen mode

AND WE WILL HAVE THE MF:

Pokemon List using module federation

And if you access the link http://localhost:5173/assets/remoteEntry.js you will see the remoteEntry manifest file:

Remot Entry File

And we finished the our first MF. Now let's consume!

Open another terminal and back to the root dir of vite. And let's create the pokemons-home, using the same command of vite:



yarn create vite pokemons-home --template react-ts


Enter fullscreen mode Exit fullscreen mode

Install the dependencies, using:



yarn


Enter fullscreen mode Exit fullscreen mode

Install the originjs/vite-plugin-federation as a dev dependency using:



yarn add -D @originjs/vite-plugin-federation


Enter fullscreen mode Exit fullscreen mode

And now let's start setting the vite.config.ts:



import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "pokemonHome",
      remotes: {
        pokemonList: "http://localhost:5173/assets/remoteEntry.js",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  build: {
    modulePreload: false,
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
});


Enter fullscreen mode Exit fullscreen mode

Note that that we have some differences. Now we have the remotes.

The remotes is where the remoteEntry is available. So we have the first app on. Because it's important we define the port to be fixed.

I will be removing the index.css and refact the App.css to:



.container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-left: 15px;
}

.pokemon-card-container {
    display: flex;
    align-items: center;
}

.pokemon-name {
    font-weight: bold;
    color: #1e3a8a;
    font-size: 20px;
}

.pokemon-image {
    width: 150px;
}


Enter fullscreen mode Exit fullscreen mode

Another thing that is very important.
Because we are using real time sharing we don't have types of typescript.
So I will rename the App.tsx to App.jsx, because when I import the MF the typescript don't complain about typing. (there's a solution for this, but it's out of the box). For this example let's just change the type of file.



import PokemonList from "pokemonList/PokemonList";
import usePokemonSelected from "pokemonList/Pokemon";

import "./App.css";

function App() {
  const [pokemon] = usePokemonSelected();

  return (
    <>
      <h3 style={{ color: "#1e3a8a", fontSize: "20px" }}>
        Created using Vite + vite-plugin-federation
      </h3>
      <PokemonList />
      {pokemon && (
        <div className="container">
          <h1 style={{ color: "#1e3a8a" }}>Selected Pokémon:</h1>
          <div className="pokemon-card-container">
            <img
              src={pokemon?.sprite}
              className="pokemon-image"
              aria-label="Image of Pokemon Selected"
            />
            <label className="pokemon-name">{pokemon?.name}</label>
          </div>
        </div>
      )}
    </>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Amazing, isn’t it? We are using the PokemonList and usePokemonSelected, exported by MF of the remoteEntry.

And that's it! We create our "host" app that will consume the MF. Run the app using:



yarn dev


Enter fullscreen mode Exit fullscreen mode

AND HERE WE ARE:

Project using the Microfrontend

We finished our two apps the pokemons-list (remote) and the pokemons-home (host).

Conclusion

The MF is amazing form to break our compoents and expose to other apps consume.
But with all in the technology world we will have trade-offs.

Imagine that the MF team, implements a bug or the app remoteEntry is not not available. We need to treat and work around the problem.

When we are using a library or components, we are using the concept of Build Time Sharing. So the component will be available on build of the app.

  • Pros

    • Complete Applications
    • Typescript Support
    • Unit or E2E Testing
  • Cons

    • No Runtime Sharing

But when we are using the MF concepts, we are using the Run Time Sharing

  • Pros

    • Not importing all component of a library
    • Runtime Sharing
  • Cons

    • Typescript Support
    • Difficult to unit and E2E testing

So you need to think and ask for yourself: I really need MF? Trade-offs... etc..

Module Federation is not the unique solution, for example single-spa

But recently the community has been adopting the module federations of webpack.

So in this example I created using Vite, but I will create the part 2 of this content, using the CRA (somethings of setting will change).

Some references:

So...

that's all folks image

Thank you so much for your support and read until here.
If possible share and like this post, it will help me a lot.

Top comments (5)

Collapse
 
betoobernal profile image
Elkin

I'm following you and the repository guide. But, i'm using scss modules and when i run the build throws this:
Image description

my vite.config is this:

import { defineConfig, UserConfig } from 'vite';
import path from 'path';
import react from '@vitejs/plugin-react-swc';
import { federation } from '@module-federation/vite';
import { createEsBuildAdapter } from '@softarc/native-federation-esbuild';
import { reactReplacements } from '@softarc/native-federation-esbuild/src/lib/react-replacements';

// https://vitejs.dev/config/
export default defineConfig(async ({command}) => {

  return {
  plugins: [
    await federation({
      options: {
        workspaceRoot: __dirname,
        outputPath: 'build',
        tsConfig: 'tsconfig.json',
        federationConfig: `module-federation/federation.config.cjs`,
        verbose: true,
        dev: command === 'serve',
      },
      adapter: createEsBuildAdapter({ plugins: [], fileReplacements: reactReplacements.dev }),
    }),
    react(),
  ],
  css: {
    preprocessorOptions: {
      scss: {
        includePaths: [
          path.resolve(__dirname, 'src/scss')
        ]
      }
    },
    modules: {
      localsConvention: 'camelCase',
    }
  },
  server: {
    fs: {
      allow: ['.'],
    },
  },
  resolve: {
    alias: [
      {
        find: "~/*",
        replacement: path.resolve(__dirname, 'src')
      },
      {
        find: "~/hooks",
        replacement: path.resolve(__dirname, 'src/hooks')
      },
      {
        find: "~/assets",
        replacement: path.resolve(__dirname, 'src/assets')
      }
    ]
  },
  build: {
    modulePreload: false,
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
} as UserConfig
})

Enter fullscreen mode Exit fullscreen mode
Collapse
 
rickeletro profile image
Ricardo Souza • Edited

Use this package: import basicSsl from "@vitejs/plugin-basic-ssl";
exemple:

import federation from "@originjs/vite-plugin-federation";
import basicSsl from "@vitejs/plugin-basic-ssl";
import react from "@vitejs/plugin-react";
import { defineConfig, loadEnv } from "vite";

// vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");

return {
plugins: [
react({ include: "*/.tsx" }),
federation({
name: "app",
remotes: {
remoteIndustrial: env.VITE_REMOTE_INDUSTRIAL,
remoteUi: env.VITE_REMOTE_COMPONENTLIB,
},
shared: ["react", "react-dom", "antd", "react-router-dom", "zustand", "@syncfusion/ej2-base"],
}),
basicSsl(),
],
server: {
port: 5001,
},
build: {
modulePreload: false,
target: "ESNext",
minify: false,
cssCodeSplit: false,
},
};
});

Collapse
 
manoharreddyporeddy profile image
Manohar Reddy Poreddy

@kevin-uehara

thanks for the details on micro frontend
for vite yarn dev is not running, giving blank page
for cra, it is working fine
any ideas? could u check if repo is running now? github.com/kevinuehara/microfronte...
is it possible that due to node package minor updates this is broken?

Collapse
 
huylv profile image
Lê Vũ Huy

Your sharing is awesome, please write a new post how can we support typescript :D

Collapse
 
idiglove profile image
Faith Morante

Thanks for this, sometimes Im confused tho if MF means Micro Frontend or Module Federation