Como eu inicio novos projetos React

Andrew Rosário
11 min readAug 28, 2023

Este artigo é uma versão adaptada para React do meu artigo Como eu inicio novos projetos Angular

Em um cenário de desenvolvimento tecnológico cada vez mais dinâmico, a criação de aplicações React já contemplando boas práticas e uma arquitetura sólida tornou-se um fator crítico para o sucesso a longo prazo. Não se trata apenas de codificar funcionalidades; é sobre construir uma base sólida que proporcione escalabilidade, manutenibilidade e uma experiência de usuário excepcional. Neste artigo, exploraremos a importância de adotar essa abordagem desde o início e como isso pode beneficiar suas aplicações React.

O Design Modular

Muitas empresas adotam o padrão de Micro Frontends para seus projetos com o objetivo de alcançar maior autonomia entre as equipes e desacoplamento de código. Porém em muitas vezes esse padrão pode trazer muita complexidade para o projeto e também problemas de performance.

Devemos sim buscar uma modularização de nossos projetos. Saber separar os recursos de um sistema acarreta em uma arquitetura mais escalável e fácil de dar manutenção.

Com isso em mente, sempre que vou criar uma nova aplicação com React, eu gosto de utilizar o Design modular do Nx.

O que é o Nx?

O Nx é um Build System com suporte para monorepos e integrações extremamente poderosas. Se você quiser saber mais sobre monorepos e o próprio Nx, eu tenho uma Talk onde me aprofundo nesses temas.

Mas então vale a pena criar novos projetos no padrão de monorepo?

A resposta é sim e não. Pensando nisso, o Nx disponibiliza uma opção para a criação de novas aplicações React Standalone, ou seja, que não possui o setup de um monorepo. Se futuramente surgir a necessidade de criar novos projetos e novas libs, será muito fácil de integrá-las nesta estrutura.

Mesmo que sua aplicação não tenha a intenção de escalar muito, ainda assim vale a pena começar com esta abordagem. Neste artigo mostrarei algumas vantagens de se utilizar o Nx em comparação com as formas tradicionais de criar aplicações React.

Ah, e não se preocupe! Neste primeiro momento você não precisa entender a fundo o que é o Nx. Apenas tenha em mente que você terá uma app React, mas com superpoderes. Ao longo do tempo seu projeto irá crescer, e é nesse momento que vale a pena investir em aprender o Nx e todos os recursos que ele te proporciona.

Criando a aplicação

Vamos criar nosso workspace com o Nx e React rodando o seguinte comando no terminal:

npx create-nx-workspace@latest my-app --preset=react-standalone

Ao rodar este comando, o Nx perguntará qual bundler você gostaria de utilizar: Vite, Webpack ou Rspack. Neste artigo utilizaremos o Vite.

Caso deseje criar o projeto com o Next.js, basta trocar o preset. (--preset=nextjs-standalone).

No primeiro momento, a estrutura gerada aparenta ser bastante semelhante com as abordagens comumente utilizadas.

Mas temos algumas diferenças que este setup entrega:

  • Setup de testes End-to-end com Cypress: Para garantir a qualidade logo de início, dentro da pasta e2e temos um ambiente todo configurado para criar testes end-to-end. Tudo isso utilizando o Cypress, que hoje é a melhor ferramenta do mercado para essa abordagem.
  • Jest pré-configurado: Para os testes unitários e testes integrados nós também já temos um setup pronto. O Jest é simplesmente o mais popular framework de testes do ecossistema JavaScript. E juntamente temos a Testing Library, que é uma lib muito popular no ecossistema React.
  • ESLint e Prettier pré-configurados: São duas ferramentas essenciais para qualquer projeto que deseja sempre manter a qualidade e padrões. Neste setup eles também já são pré-configurados.

Note a presença de dois arquivos que serão importantes no futuro: nx.json e project.json. Muitas das configurações de seu workspace serão feitas a partir deles. Mais uma vez ressaltando: conforme seu projeto for crescendo, vale a pena entender as responsabilidades destes arquivos e como eles influenciam em sua arquitetura.

Servindo a aplicação

Para rodar a aplicação localmente, basta executar npm start ou então npx nx serve. Perceba que estamos sempre executando os processos pelo Nx. Mais pra frente entenderemos as suas vantagens.

Além do serve, temos outros comandos úteis que serão utilizados frequentemente:

npx nx build # executa o build da aplicação
npx nx test # roda os testes utilizando o Jest
npx nx lint # roda o lint utilizando o ESLint
npx nx e2e e2e # roda os testes e2e utilizando o Cypress

Criando nosso primeiro componente

O Nx possui diversos plugins para geração de código que chamamos de Generators. Os Generators permitem que você crie códigos, configurações ou projetos inteiros com facilidade.

Para criar nosso componente, vamos executar o seguinte comando:

npx nx g @nx/react:component hello-world

Dica: o Nx possui muitos Generators, e nem sempre vamos lembrar os nomes de todos eles, portanto recomendo fortemente instalar a extensão Nx Console para o VSCode. Ela proporciona uma interface para poder executar qualquer tipo de comando e também ir muito além.

Modularização: a grande vantagem

Dado todo esse contexto, já conseguimos avançar no desenvolvimento da aplicação, mas você pode estar se perguntando se realmente vale a pena trocar o método padrão para utilizar o Nx. O diferencial está no momento de separar as responsabilidades, ou seja, dividir nosso sistema em vários módulos.

Quando você desenvolve um projeto React, geralmente toda a sua lógica fica na pasta app ou src. Idealmente separados por vários nomes de pastas que representam seus domínios. À medida que a app cresce, ela se torna cada vez mais monolítica.

Nesta estrutura, vamos separar nossa lógica em libs locais. Com isso temos os seguintes ganhos:

  • Melhor separação de responsabilidades
  • Maior reutilização
  • “APIs” mais explícitas entre suas “áreas de domínio”
  • Maior escalabilidade em CI habilitando comandos de test/lint/build independentes para cada lib
  • Maior escalabilidade em suas equipes, permitindo que equipes diferentes trabalhem em libs separadas

Vamos supor que nossas áreas de domínio incluam produtos, pedidos e alguns componentes UI (User Interface) mais genéricos. Podemos gerar uma nova lib para cada uma dessas áreas usando o Generator de libs do React:

npx nx g @nx/react:library products --directory=modules
npx nx g @nx/react:library orders --directory=modules
npx nx g @nx/react:library ui --directory=modules/shared

Temos agora a seguinte estrutura dividida na pasta modules :

Não se preocupe com a quantidade de arquivos que foram gerados para cada lib. O Nx já cria um setup otimizado para cada uma delas. Mas recomendo explorar os arquivos criados. Perceba que você pode realizar diferentes configurações pra cada um dos módulos. Isso proporciona muito mais flexibilidade, principalmente quando se está trabalhando com várias equipes.

Podemos pensar que todos esses módulos são independentes, por mais que no final a sua app irá importar todos eles.

Imagine um cenário onde uma equipe será responsável por desenvolver as features de produtos. Ela atuará somente na lib products. Os componentes, services e qualquer outro tipo de código será criado dentro desta lib. E esta equipe será responsável por definir o que as outras camadas poderão enxergar (sua API pública) por meio do arquivo index.ts.

Além disso, como o seu escopo é de produtos, não há a necessidade de escrever e rodar testes de outros escopos. Queremos executar os testes somente desta lib. E o mesmo vale para o lint.

npx nx test modules-products
npx nx lint modules-products

Integrando libs

Sabemos que a lib shared de ui pode ser importada em outros domínios. Podemos então importar um componente genérico dentro do componente de produtos.

import { ModulesSharedUi } from 'modules/shared/ui';
import styles from './product-list.module.scss';

export function ProductList() {
return (
<div className={styles['container']}>
<h1>Welcome to Product List!</h1>
<ModulesSharedUi />
</div>
);
}

export default ProductList;

O componente ModulesSharedUi pode ser importado normalmente no módulo de produtos e em qualquer outro módulo. O mais interessante aqui é o path de onde ele está sendo importado: modules/shared/ui.

Toda vez que criamos uma lib com o Generator, o Nx automaticamente adiciona um path alias para ela. Podemos visualizar todos os alias e editá-los no arquivo tsconfig.base.json.

{
"compilerOptions": {
...
"paths": {
"modules/orders": ["modules/orders/src/index.ts"],
"modules/products": ["modules/products/src/index.ts"],
"modules/shared/ui": ["modules/shared/ui/src/index.ts"]
}
},
...
}

Agora queremos criar uma rota /products que será configurada via React Router DOM.

npm install react-router-dom

Primeiramente vamos configurar o roteador no arquivo main.tsx.

import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';

import App from './app/app';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);

root.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);

E agora vamos utilizar a mesma abordagem de antes relacionada aos imports de módulos. Basta importar a lib de products para o nosso componente onde se encontram as rotas, no caso o app.tsx.

import { Route, Routes } from 'react-router-dom';
import { ProductList } from 'modules/products';

function Home() {
return <h1>Home</h1>;
}

export function App() {
return (
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/products" element={<ProductList />}></Route>
</Routes>
);
}

export default App;

Se quiséssemos criar uma rota /orders com a lib de orders é só seguir o mesmo processo.

Visualizando a estrutura do seu projeto

À medida que o seu projeto cresce e muitas libs são criadas, precisamos ter mais controle de como a arquitetura está escalando. Felizmente o Nx possui uma feature muito interessante que automaticamente detecta todas as dependências e monta uma estrutura de árvore.

Basta executar o seguinte comando:

npx nx graph

Veja que interessante este grafo. A lib modules-shared-ui é importada pelas nossas duas libs de negócio: modules-order e modules-products. E a nossa app representada pelo my-app importa nossas libs de negócio.

Conhecendo o Affected

Se com todas essas funcionalidades, você ainda não está convencido de utilizar o Nx, após entender como funciona o affected você mudará de ideia!

Imagine que a equipe responsável pela feature de orders fez uma pequena alteração em um dos componentes da lib dela. Já entendemos que temos a possibilidade de executar os comandos de build e lint somente de module-orders. Mas isso talvez não seja o suficiente.

Se olharmos novamente o grafo gerado, percebemos que my-app tem uma dependência de module-orders. Pode acontecer de algum código modificado de module-orders afetar diretamente em nossa aplicação. Como não rodamos os testes de my-app, não ficaremos cientes disso.

É nesse momento que o affected entra em ação. Vamos gerar o mesmo grafo, mas agora queremos que o Nx nos mostre quem está sendo afetado com as modificações atuais.

npx nx affected:graph

Sensacional! O Nx é inteligente o suficiente para entender que module-orders foi alterado e consequentemente todos os projetos que dependem dele na árvore de dependências também!

E o melhor de tudo: nós não estamos limitados a somente visualizar quem foi afetado, mas também executar qualquer comando affected!

npx nx affected:test
npx nx affected:lint
Output do Nx para os testes afetados

Por mais que sua aplicação seja extremamente enorme, separando-a em libs e utilizando o affected para todos os processos, ela terá sempre uma boa velocidade de execução, pois estaremos escalando ela e focando no que realmente importa, evitando executar processos desnecessários.

Configurando o Nx na sua Pipeline

Agora vem a cereja do bolo. Vamos automatizar todos esses processos que aprendemos até aqui em nosso Continuous Integration (CI). Para exemplo, utilizarei o Github Actions.

Vamos criar o arquivo .github/workflows/ci.yml na raiz do nosso projeto com as seguintes instruções:

name: CI
on:
push:
branches:
- main
pull_request:

jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v3
- run: npm ci

- run: npx nx format:check
- run: npx nx affected -t lint --parallel=3
- run: npx nx affected -t test --parallel=3 --configuration=ci
- run: npx nx affected -t build --parallel=3

O mais importante é focar nos steps: perceba que temos uma etapa para cada processo: lint, test e build do que foi afetado (affected).

Basicamente ao subir esse arquivo para um repositório no Github, vamos garantir que qualquer push para a branch main ou na abertura de qualquer Pull Request, o CI será triggado executando sempre todas as etapas.

Tendo o CI configurado, vamos commitar a alteração da lib module-orders e realizar um push para o repositório.

Feito isso, podemos acompanhar a execução do nosso CI pela aba de Actions do nosso repositório:

Vamos analisar o que o Nx executou no step de test, por exemplo:

Perfeito! o mesmo output local foi executado no CI. Não foi necessário executar os testes da lib modules-shared-ui e nem da modules-products porque elas não foram afetadas. Imagine o ganho que podemos ter na performance e redução de custos da nossa pipeline!

O modelo mental

O que construímos até aqui vai de encontro com a necessidade de muitos projetos que precisam crescer exponencialmente. A separação de responsabilidades e a capacidade de escala é levada a sério, tudo isso sem abrir mão da performance.

Por mais que tenhamos diversas libs, no final, o build e o deploy é somente um. As técnicas essenciais para a velocidade de uma app React continuam as mesmas.

Se por algum motivo você precisar mover uma lib para um outro repositório, a arquitetura do seu projeto permite isso com muita facilidade. Ou você pode seguir a estrutura de monorepo, elevando ainda mais o reuso de código.

Esse ponto pode ser crucial na escolha da arquitetura do seu projeto. Você talvez não precise ter vários deploys independentes e reinventar a roda pra alcançar uma boa performance (sim, microfrontend, estou olhando para você 👀).

Na maioria dos casos o que você precisa é ter um workflow que te traga confiança nas entregas contínuas, sem abrir mão da autonomia entre equipes. E tudo isso o Nx disponibiliza com maestria.

Para se aprofundar mais

E não para por aqui. Existem muitas outras vantagens que o Nx oferece. Deixo aqui mais dois recursos que podem fazer toda a diferença para a qualidade e performance de todo o seu workflow de desenvolvimento:

  • Module Boundary Rules: Definições de regras no ESLint para garantir o controle das dependências de suas libs. Exemplo: garantir que module-orders e module-products somente dependa de module-shared-ui e não o contrário.
  • Nx Cloud: É possível integrar esta solução na nuvem do Nx para realizar o cache de todos os seus outputs (lint, test, build, etc), garantindo assim uma maior performance e redução de custos do seu CI.

Conclusão

O Nx é uma solução extremamente robusta e eficiente para gerenciar projetos React de qualquer tamanho. Desde aplicações simples com dois módulos até projetos enormes com diversas equipes.

Seu ferramental possibilita muitos ganhos para todo o fluxo de desenvolvimento. Cada vez mais empresas vêm adotando esse padrão para garantir uma arquitetura mais robusta e escalável.

Confira o repositório no Github com o projeto de exemplo deste artigo.
E aproveita para me seguir no Linkedin e no Twitter. 😀 Estou sempre postando conteúdos sobre Programação e Frontend!

--

--

Andrew Rosário

Desenvolvedor Front-end, mentor e palestrante. Apaixonado por tecnologia e por compartilhar conhecimento.