Tutorial: Criando um Micro Frontend utilizando React, Next.js, Typescript e Module Federation
Tags: react, nextjs, modulefederation, braziliandevs
Published: 2022-05-31 23:34:38

### Tabela de Conteúdos
- [Introdução](#introdução)
- [Termos utilizados](#termos-utilizados)
- [Criar o monorepo do projeto](#criar-o-monorepo-do-projeto)
- [Criar o host com o Next.js](#criar-o-host-com-o-nextjs)
- [Configurar Typescript no Next.js](#configurar-typescript-no-nextjs)
- [Criar projeto remote com React, Typescript e Webpack 5](#criar-projeto-remote-com-react-typescript-e-webpack-5)
- [Configurar o Module Federation](#configurar-o-module-federation)
- [Conclusão](#conclusão)
- [Repositório no Github](#repositório-no-github)
- [Referências](#referências)

---

## 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:

```shell
mkdir next-react-typescript-mfe

cd next-react-typescript-mfe
```

Vamos iniciar nosso projeto:

```shell
yarn init -y -p
git init # opcional caso queira realizar o controle de versão com o Git
```

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

```shell
yarn add -D typescript @types/react @types/react-dom \
  @types/node concurrently
```

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**
```gitignore
node_modules
```

---

## Criar o host com o Next.js

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

```shell
npx create-next-app host
```

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:**
```json
{
  // ...
  "workspaces": ["host"], // Adicionar aqui
  // ...
}
```

---

## 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.

```shell
touch host/tsconfig.json
```

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:

```shell
yarn workspace host dev
```

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:


```shell
mv host/pages/_app.js host/pages/_app.tsx
mv host/pages/index.js host/pages/index.tsx
```

---

## 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:

```shell
mkdir remote
cd remote
yarn init -y -p
```

Podemos voltar para a raiz do projeto:

```shell
cd ..
```

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

**package.json:**
```json
{
  // ...
  "workspaces": [
    "host",
    "remote" // Adicionar aqui
  ],
  // ...
}
```

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

```shell
yarn workspace remote add react react-dom
```

E mais a algumas dependências de desenvolvimento:

```shell
yarn workspace remote add -D webpack webpack-cli \
  webpack-dev-server html-webpack-plugin css-loader \
  source-map-loader style-loader ts-loader
```

Agora necessitamos criar as pastas dentro do projeto:

```shell
cd remote
mkdir src
mkdir public
cd ..
```

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

```shell
touch remote/src/App.tsx
```
**remote/src/App.tsx:**
```typescript
import React from "react";

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

export default App;
```

```shell
touch remote/src/index.tsx
```
**remote/src/index.tsx:**
```typescript
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 />);
```

```shell
touch remote/public/index.html
```
**remote/public/index.html:**
```html
<!DOCTYPE html>
<html lang="en">

<head> </head>

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

</html>
```

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

```shell
touch remote/tsconfig.json
```
**remote/tsconfig.json:**
```json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}
```

```shell
touch remote/webpack.config.js
```
**remote/webpack.config.js:**
```javascript
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",
    }),
  ],
};
```

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

**remote/package.json:**
```json
{
  // ...
  "scripts": {
    "start": "webpack-dev-server --port 3001"
  },
  // ...
}
```

**package.json:**
```json
{
  // ...
  "scripts": {
    "start": "concurrently \"yarn workspace host dev\" \"yarn workspace remote start\""
  },
  // ...
}
```

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

```shell
yarn
```

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qnciuu5xkacok0bvexcj.png)

Para resolver, basta adicionar o item `moduleResolution`:

**host/tsconfig.json:**
```json
{
  "compilerOptions": {
    // ...
    "moduleResolution": "node",
    "resolveJsonModule": true,
    // ...
  },
  // ...
}
```
---

## 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:**
```typescript
import React from "react";

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

export default Button;
```

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

**remote/webpack.config.js:**
```javascript
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,
        },
      },
    }),
  ],
};
```

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

```shell
yarn workspace host add @module-federation/nextjs-mf@2.3.1
```

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

**host/next.config.js:**
```javascript
/** @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
```

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

**host/pages/index.tsx:**
```typescript
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>
  )
}

```

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:**
```typescript
/// <reference types="react" />

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

  export default Button;
}
```

Tudo pronto! É so rodar o projeto...

```shell
yarn start
```

... 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](https://github.com/patrickcoutinho/next-react-typescript-mfe)

---

## Referências

[Module Federation Examples](https://github.com/module-federation/module-federation-examples/)

[Module Federation For Next.js](https://app.privjs.com/buy/packageDetail?pkg=@module-federation/nextjs-mf)

[Module Federation Docs](https://webpack.js.org/concepts/module-federation/)

[Building React App with Module Federation and NextJS/React](https://dev.to/omher/building-react-app-with-module-federation-and-nextjsreact-1pkh)