DEV Community

Cover image for Tutorial: Criando um Micro Frontend utilizando React, Next.js, Typescript e Module Federation
Patrick Coutinho
Patrick Coutinho

Posted on • Updated on

Tutorial: Criando um Micro Frontend utilizando React, Next.js, Typescript e Module Federation

Tabela de Conteúdos


Introdução

Certamente você, se é da área tech, já ouviu falar sobre Micro Frontends, e provavelmente também ouviu a respeito do Module Federation.

Um Micro Frontend é basicamente a extensão do conceito de micro serviços para o Frontend. Já o Module Federation é um recurso do Webpack 5 que eleva a construção de Micro Frontends para um novo patamar. Pretendo abordar mais conceitualmente sobre estes temas em outro post.

Esse tem como objetivo partir para a prática e mostrar como criar do zero a estrutura simples para um projeto de Micro Frontend utilizando React, Next.js, Typescript e Module Federation.

Vamos lá!


Termos utilizados

Primeiro, vamos explicar alguns termos que utilizaremos ao longo do post:

HOST: Trata-se da aplicação central (shell) que será responsável por carregar os componentes remotos federados. Vamos utilizar o Next.js aqui.

REMOTE: É a aplicação que vai compartilhar componentes com o HOST. Será construída com o React, sem utilizar o CRA.

Vamos para nosso passo a passo:


Criar o monorepo do projeto

Agora é a hora de abrir o terminal e vamos codar!

Começamos criando a pasta do projeto:

mkdir next-react-typescript-mfe

cd next-react-typescript-mfe
Enter fullscreen mode Exit fullscreen mode

Vamos iniciar nosso projeto:

yarn init -y -p
git init # opcional caso queira realizar o controle de versão com o Git
Enter fullscreen mode Exit fullscreen mode

Por enquanto as únicas dependências que vamos instalar é o Typescript, o Concurrently e algumas tipagens:

yarn add -D typescript @types/react @types/react-dom \
  @types/node concurrently
Enter fullscreen mode Exit fullscreen mode

Estas dependências vão ficar compartilhadas com os projetos que teremos dentro do nosso monorepo. Para gerenciar o monorepo vamos usar o Yarn Workspaces.

Podemos também adicionar um arquivo .gitignore com o seguinte conteúdo (opcional):

.gitignore

node_modules
Enter fullscreen mode Exit fullscreen mode

Criar o host com o Next.js

Para criar nosso projeto HOST, vamos digitar o seguinte comando:

npx create-next-app host
Enter fullscreen mode Exit fullscreen mode

Ao final do processo teremos nossa pasta host com a instalação do Next.js prontinha.

Finalizado o processo anterior, podemos adicionar o projeto host nas configurações do workspace, dentro do package.json na raiz do projeto:

package.json:

{
  // ...
  "workspaces": ["host"], // Adicionar aqui
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Configurar Typescript no Next.js

Para configurar o Typescript é bem simples, basta criar o arquivo tsconfig.json dentro da pasta host e realizar alguns pequenos ajustes.

touch host/tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Na próxima vez que iniciarmos o projeto será criado o next-env.d.ts e serão populadas configurações no tsconfig.json.

Para iniciar podemos rodar o comando:

yarn workspace host dev
Enter fullscreen mode Exit fullscreen mode

O projeto host está configurado, é hora de renomear nossos arquivos para que fiquem com a extensão ts ou tsx. Para isso você pode usar sua IDE (VS Code por exemplo), gerenciador de arquivos ou via linha de comando:

mv host/pages/_app.js host/pages/_app.tsx
mv host/pages/index.js host/pages/index.tsx
Enter fullscreen mode Exit fullscreen mode

Criar projeto remote com React, Typescript e Webpack 5

Momento de criarmos nossa aplicação remota. Vamos ter um pouco mais de trabalho aqui, pois não vamos usar o create-react-app para que tenhamos maior controle das configurações.

Começamos criando a pasta do projeto e iniciando o projeto:

mkdir remote
cd remote
yarn init -y -p
Enter fullscreen mode Exit fullscreen mode

Podemos voltar para a raiz do projeto:

cd ..
Enter fullscreen mode Exit fullscreen mode

Precisamos adicionar o projeto ao workspace, da mesma forma que fizemos com o host:

package.json:

{
  // ...
  "workspaces": [
    "host",
    "remote" // Adicionar aqui
  ],
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar o react e o react-dom ao projeto:

yarn workspace remote add react react-dom
Enter fullscreen mode Exit fullscreen mode

E mais a algumas dependências de desenvolvimento:

yarn workspace remote add -D webpack webpack-cli \
  webpack-dev-server html-webpack-plugin css-loader \
  source-map-loader style-loader ts-loader
Enter fullscreen mode Exit fullscreen mode

Agora necessitamos criar as pastas dentro do projeto:

cd remote
mkdir src
mkdir public
cd ..
Enter fullscreen mode Exit fullscreen mode

E também os arquivos App.tsx, index.tsx e index.html:

touch remote/src/App.tsx
Enter fullscreen mode Exit fullscreen mode

remote/src/App.tsx:

import React from "react";

const App = (): JSX.Element => {
  return (
    <>
      <div>React Remote</div>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode
touch remote/src/index.tsx
Enter fullscreen mode Exit fullscreen mode

remote/src/index.tsx:

import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const container = document.getElementById("root");
const root = createRoot(container!);

root.render(<App />);
Enter fullscreen mode Exit fullscreen mode
touch remote/public/index.html
Enter fullscreen mode Exit fullscreen mode

remote/public/index.html:

<!DOCTYPE html>
<html lang="en">

<head> </head>

<body>
  <div id="root"></div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Neste momento precisamos adicionar os arquivos de configuração do webpack e do typescript:

touch remote/tsconfig.json
Enter fullscreen mode Exit fullscreen mode

remote/tsconfig.json:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode
touch remote/webpack.config.js
Enter fullscreen mode Exit fullscreen mode

remote/webpack.config.js:

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index",
  target: "web",
  mode: "development",
  devtool: "source-map",
  resolve: {
    extensions: [".jsx", ".js", ".tsx", ".ts", ".json"],
  },
  module: {
    rules: [
      {
        enforce: "pre",
        test: /\.js$/,
        loader: "source-map-loader",
      },
      {
        test: /\.(ts|tsx)$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Precisamos também adicionar scripts nos arquivos package.json da raiz e do remote:

remote/package.json:

{
  // ...
  "scripts": {
    "start": "webpack-dev-server --port 3001"
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

package.json:

{
  // ...
  "scripts": {
    "start": "concurrently \"yarn workspace host dev\" \"yarn workspace remote start\""
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

E para finalizar, rodamos o install para atualizar as dependências:

yarn
Enter fullscreen mode Exit fullscreen mode

Neste momento sua IDE (no caso do print, o VS Code) pode estar acusando o seguinte erro no arquivo host/tsconfig.json:

Image description

Para resolver, basta adicionar o item moduleResolution:

host/tsconfig.json:

{
  "compilerOptions": {
    // ...
    "moduleResolution": "node",
    "resolveJsonModule": true,
    // ...
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Configurar o Module Federation

Nesta etapa é que a mágica vai acontecer!

Vamos começar pelo REMOTE, nosso projeto React, criando um simples componente para ser federado e consumido pelo host:

remote/src/components/Button.tsx:

import React from "react";

const Button = (): JSX.Element => {
  return (
    <>
      <button>Remote Button</button>
    </>
  );
};

export default Button;
Enter fullscreen mode Exit fullscreen mode

Também precisamos adicionar algumas configurações do Webpack:

remote/webpack.config.js:

const HtmlWebpackPlugin = require("html-webpack-plugin");

const ModuleFederationPlugin =
  require("webpack").container.ModuleFederationPlugin;

module.exports = {
  entry: "./src/index",
  target: "web",
  mode: "development",
  devtool: "source-map",
  resolve: {
    extensions: [".jsx", ".js", ".tsx", ".ts", ".json"],
  },
  module: {
    rules: [
      {
        enforce: "pre",
        test: /\.js$/,
        loader: "source-map-loader",
      },
      {
        test: /\.(ts|tsx)$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
        new ModuleFederationPlugin({
      name: "remote",
      filename: "remoteEntry.js",
      exposes: {
        "./Button": "./src/components/Button",
      },
      shared: {
        react: {
          requiredVersion: false,
          singleton: true,
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Depois vamos configurar nosso projeto HOST com Next.js. Para ele vamos precisar instalar um plugin:

yarn workspace host add @module-federation/nextjs-mf@2.3.1
Enter fullscreen mode Exit fullscreen mode

Também temos algumas alterações no next.config.js:

host/next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack5: true,
  reactStrictMode: true,
  webpack(config, options) {
    const { webpack, isServer } = options;
    config.experiments = { topLevelAwait: true };

    config.module.rules.push({
      test: /_app.js/,
      loader: "@module-federation/nextjs-mf/lib/federation-loader.js",
    });

    config.plugins.push(
      new webpack.container.ModuleFederationPlugin({
        remotes: {
          remote: "remote@http://localhost:3001/remoteEntry.js",
        },
        shared: {
          react: {
            singleton: true,
            eager: true,
            requiredVersion: false,
          },
        },
      })
    );
    return config;
  },
}

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

E finalmente vamos importar o Button exposto pelo REMOTE em nosso index:

host/pages/index.tsx:

import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import dynamic from 'next/dynamic'; // new

 // new
const RemoteButton = dynamic(() => import('remote/Button'), {
  ssr: false,
});

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      {/** new */}
      <RemoteButton />

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.js</code>
        </p>

        <div className={styles.grid}>
          <a href="https://nextjs.org/docs" className={styles.card}>
            <h2>Documentation &rarr;</h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a href="https://nextjs.org/learn" className={styles.card}>
            <h2>Learn &rarr;</h2>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/canary/examples"
            className={styles.card}
          >
            <h2>Examples &rarr;</h2>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            className={styles.card}
          >
            <h2>Deploy &rarr;</h2>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Neste momento a análise estática do typescript estará alertando pelo tipo desconhecido do Button. Para solucionar, basta criarmos o type definition:

host/remote.d.ts:

/// <reference types="react" />

declare module "remote/Button" {
  const Button: React.ComponentType;

  export default Button;
}
Enter fullscreen mode Exit fullscreen mode

Tudo pronto! É so rodar o projeto...

yarn start
Enter fullscreen mode Exit fullscreen mode

... e acessar o HOST pelo endereço http://localhost:3000 e veremos o botão do REMOTE sendo exibido.


Conclusão

Pronto! Se deu tudo certo na execução dos passos acima, você tem seu projeto de Micro Frontend utilizando Module Federation rodando em sua máquina. Legal, né? E também é mais simples do que parece, não é verdade?

Se gostou do post, se ele foi útil para você, deixe sua reação ao post e também aproveite para seguir meu perfil aqui no dev.to. Em breve farei novas postagens sobre o assunto.


Repositório no Github

https://github.com/patrickcoutinho/next-react-typescript-mfe


Referências

Module Federation Examples

Module Federation For Next.js

Module Federation Docs

Building React App with Module Federation and NextJS/React

Top comments (4)

Collapse
 
lucasfrutig0 profile image
lucasfrutig0

Faz sentido usar module federation pra servir 2 apps distintos? Com logo diferente, cores diferentes, mas que por ventura possam compartilhar certas features?

Collapse
 
patrickcoutinho profile image
Patrick Coutinho

Olá, Lucas. Faz sentido sim, seus componentes vão herdar essas características, como cores, do host, principalmente se você estiver utilizando algum tipo de tema.

Caso as características estejam hard coded no componente, aí basta você abstrair elas para o tema, ou passar por props, caso do logotipo por exemplo.

Collapse
 
lucasfrutig0 profile image
lucasfrutig0

Show, nesse caso eu poderia usar variáveis de ambiente pra determinar qual tema aplicar, ou existe alguma forma mais interessante pra fazer essa diferenciação?

Thread Thread
 
patrickcoutinho profile image
Patrick Coutinho

Vou usar como exemplo o tailwind css:

Digamos que vc tem a seguinte estrutura:

  1. Components (Federalizados, seria o remote)
  2. App1 (seria um host)
  3. App2 (seria um host também)

No teu Components você criaria uma nomenclatura para cores, por exemplo primary, secondary, tertiary... no tailwind.config.js vc teria a configuração dessas cores para desenvolvimento.

Teus App1 e App2 teriam seus tailwind.config.js cada um com suas respectivas cores. Então quando vc utilizasse o componente federalizado ele assumiria as cores do App host.

No tailwind.config.js você pode usar css variables, então na prática essas cores poderiam vir até de um banco de dados e serem gerenciadas por um CMS.