Nota: apenas traduzi o texto abaixo e postei aqui. As referências estão no fim deste artigo.

cats

Como estruturar React apps "da maneira certa" parece ser o assunto do momento recentemente, desde que o React existe. A opinião oficial do React sobre isso é que ele "não tem opiniões". Isso é ótimo, nos dá liberdade total para fazer o que quisermos. E também é ruim. Isso leva a tantas opiniões fundamentalmente diferentes e muito fortes sobre a estrutura adequada do React app, que até mesmo os desenvolvedores mais experientes às vezes se sentem perdidos, sobrecarregados e precisam chorar em um canto escuro por causa disso.

Eu, é claro, também tenho uma opinião forte sobre o tópico 😈. E nem vai ser "depende" dessa vez 😅 (quase). O que quero compartilhar hoje é o sistema, que vi funcionando muito bem em:

  • um ambiente com dezenas de equipes vagamente conectadas no mesmo repositório trabalhando no mesmo produto
  • em um ambiente acelerado de uma pequena startup com apenas alguns engenheiros
  • ou mesmo para projetos de uma pessoa (sim, eu uso isso o tempo todo para minhas coisas pessoais)

Só lembre-se, assim como o Código do Pirata, tudo isso é mais o que você chamaria de "diretrizes" do que regras reais.

O que precisamos da convenção de estrutura de projeto

Não quero entrar em detalhes sobre por que precisamos de convenções como essa em primeiro lugar: se você chegou a este artigo, provavelmente já decidiu que precisa dela. O que eu quero falar um pouco, antes de pular para soluções, é o que torna uma convenção de estrutura de projeto ótima.

Replicabilidade

A convenção de código deve ser compreensível e fácil o suficiente para ser reproduzida por qualquer membro da equipe, incluindo um estagiário recém-contratado com experiência mínima em React. Se a maneira de trabalhar em seu repositório exige um PhD, alguns meses de treinamento e debates profundamente filosóficos sobre cada segundo PR... Bem, provavelmente será um sistema realmente bonito, mas não existirá em nenhum outro lugar além do papel.

Inferrabilidade

Você pode escrever um livro e gravar alguns filmes sobre "A maneira de trabalhar em nosso repositório". Você provavelmente pode até convencer todos na equipe a ler e assistir (embora você provavelmente não vá). O fato é: a maioria das pessoas não vai memorizar cada palavra, se é que vai. Para que a convenção realmente funcione, ela deve ser tão óbvia e intuitiva, de modo que as pessoas na equipe idealmente sejam capazes de fazer engenharia reversa apenas lendo o código. No mundo perfeito, assim como com comentários de código, você nem precisaria escrevê-lo em lugar nenhum - o código e a estrutura em si seriam sua documentação.

Independência

Um dos requisitos mais importantes das diretrizes de estrutura de codificação para várias pessoas, e especialmente várias equipes, é solidificar uma maneira para os desenvolvedores operarem independentemente. A última coisa que você quer é vários desenvolvedores trabalhando no mesmo arquivo, ou equipes constantemente invadindo as áreas de responsabilidade umas das outras.

Portanto, nossas diretrizes de estrutura de codificação devem fornecer tal estrutura, onde as equipes sejam capazes de coexistir pacificamente dentro do mesmo repositório.

Otimizado para refatoração

Último, mas no mundo moderno do frontend, é o mais importante. O frontend hoje em dia é incrivelmente fluido. Patterns, frameworks e melhores práticas estão mudando constantemente. Além disso, espera-se que entreguemos features rapidamente hoje em dia. Não, RÁPIDO. E então reescrevê-lo completamente depois de um mês. E então talvez reescrevê-lo novamente.

Então se torna muito importante para nossa convenção de codificação não nos forçar a "colar" o código em algum lugar permanente sem nenhuma maneira de movê-lo. Ela deve organizar as coisas de tal forma que a refatoração seja algo que seja realizado casualmente em uma base diária. A pior coisa que uma convenção pode fazer é tornar a refatoração tão difícil e demorada que todos fiquem apavorados com ela. Em vez disso, deve ser tão simples quanto respirar.

...

Agora que temos nossos requisitos gerais para a convenção de estrutura do projeto, é hora de entrar em detalhes. Vamos começar com o panorama geral e, então, detalhar.

Organizando o projeto em si: decomposition

A primeira e mais importante parte da organização de um grande projeto que esteja alinhado com os princípios que definimos acima é a "decomposition": em vez de pensar nisso como um projeto monolítico, pode ser pensado como uma composição de recursos mais ou menos independentes. A boa e velha discussão "monólito" vs "microsserviços", apenas dentro de um React app. Com essa abordagem, cada feature é essencialmente um "nanoserviço" de certa forma, que é isolado do restante das features e se comunica com elas por meio de uma "API" externa (geralmente apenas React props).

components graph

Mesmo apenas seguindo essa mentalidade, comparado à abordagem mais tradicional do "projeto React", você terá praticamente tudo da nossa lista acima: equipes/pessoas poderão trabalhar independentemente em features em paralelo se as implementarem como um monte de "black boxes" conectadas umas às outras. Se a configuração estiver correta, deve ser bem óbvio para qualquer um também, só exigiria um pouco de prática para se ajustar à mudança de mentalidade. Se você precisar remover uma feature, você pode simplesmente "desconectá-la" ou substituí-la por outra feature. Ou se você precisar refatorar os detalhes internos de uma feature, você pode fazer isso. E enquanto a "API" pública dele permanecer funcional, ninguém de fora vai nem perceber.

Estou descrevendo um React component, não é? 😅 Bem, o conceito é o mesmo, e isso torna o React perfeito para essa mentalidade. Eu definiria uma "feature", para distingui-lo de um "component", como "um monte de components e outros elements unidos em uma funcionalidade completa da perspectiva do usuário final".

Agora, como organizar isso para um único projeto? Especialmente considerando que, comparado a microsserviços, ele deve vir com muito menos encanamento: em um projeto com centenas de features, extraí-los todos para microsserviços reais será quase impossível. O que podemos fazer em vez disso é usar a arquitetura multi-package monorepo: é perfeita para organizar e isolar features independentes como packages. Um package é um conceito que já deve ser familiar para qualquer um que instalou algo do npm. E um monorepo - é apenas um repo, onde você tem o código-fonte de vários packages vivendo juntos em harmonia, compartilhando ferramentas, scripts, dependências e, às vezes, uns aos outros.

Então o conceito é simples: projeto React → dividi-lo em features independentes → colocar essas features em packages.

components graph 2

Se você nunca trabalhou com monorepo configurado localmente e agora, depois que mencionei "package" e "npm", se sente desconfortável com a ideia de publicar seu projeto privado: não fique. Nem publicação nem código aberto são requisitos para que um monorepo exista e para que os desenvolvedores obtenham os benefícios dele. Da perspectiva do código, um package é apenas uma pasta, que tem o arquivo package.json com algumas propriedades. Essa pasta é então linked por meio dos links simbólicos do Node à pasta node_modules, onde os packages "tradicionais" são instalados. Esse linking é realizado por ferramentas como Yarn ou Npm: é chamada de "workspaces", e ambos oferecem suporte a isso. E eles tornam os packages acessíveis em seu código local como qualquer outro package baixado do npm.

Ficaria assim:

/packages
  /my-feature
    /some-folders-in-feature
    index.ts
    package.json // isto é o que define o my-feature package
  /another-feature
    /some-folders-in-feature
    index.ts
    package.json // isto é o que define o another-feature package

e no package.json eu teria esses dois campos importantes:

{
  "name": "@project/my-feature",
  "main": "index.ts"
}

Onde o campo "name" é, obviamente, o nome do package - basicamente o alias para esta pasta, através do qual ele será acessível ao código no repositório. E "main" é o entry point principal para o package, ou seja, qual arquivo será importado quando eu escrever algo como

import { Something } from '@project/my-feature';

Existem alguns repositórios públicos de projetos conhecidos que usam a abordagem monorepo de vários packages: Babel, React, Jest, para citar alguns.

Por que packages em vez de apenas folders

À primeira vista, a abordagem dos packages parece "apenas dividir seus recursos em folders, qual é o problema" e não parece tão inovadora. No entanto, há algumas coisas interessantes que os packages podem nos dar, que folders simples não podem.

Alias. Com packages, você pode se referir à sua feature pelo nome, não pela localização. Compare isto:

import { Button } from '@project/button';

com esta abordagem mais "tradicional":

import { Button } from '../../components/button';

No primeiro import, é óbvio - estou usando um generic component de "button" do meu projeto, minha versão de design system.

Na segunda, não está tão claro - o que é esse button? É o generic button de "design system"? Ou talvez parte dessa feature? Ou uma feature "acima"? Posso usá-la aqui, talvez tenha sido escrito para algum caso de uso muito específico que não vai funcionar na minha nova feature?

Fica ainda pior se você tiver vários folders "utils" ou "common" no seu repositório. Meu pior pesadelo de código se parece com isso:

import { bla } from '../../../common';
import { blabla } from '../../common';
import { blablabla } from '../common';

Com packages, poderia ser algo como isto:

import { bla } from '@project/button/common';
import { blabla } from '@project/something/common';
import { blablabla } from '@project/my-feature/common';

Instantaneamente óbvio o que vem de onde e o que pertence a onde. E as chances são de que o código "my-feature" "common" foi escrito apenas para uso interno da feature, nunca foi feito para ser usado fora da feature, e reutilizá-lo em outro lugar é uma má ideia. Com pacotes, você verá isso imediatamente.

Separation of concerns. Considerando que todos nós estamos acostumados com os npm packages e o que eles representam, fica muito mais fácil pensar sobre sua feature como um module isolado com sua própria API pública quando ele é escrito como um "package" imediatamente.

Dê uma olhada nisso:

import { dateTimeConverter } from '../../../../button/something/common/date-time-converter';

vs isso:

import { dateTimeConverter } from '@project/button';

O primeiro provavelmente se perderá em todos os imports ao redor e passará despercebido, transformando seu código em The Big Ball of Mud. O segundo levantará algumas sobrancelhas instantâneamente e naturalmente: um date time converter? De um button? Sério? O que naturalmente forçará limites mais claros entre diferentes features/packages.

Suporte integrado. Você não precisa inventar nada, a maioria das ferramentas modernas, como IDE, typescript, linting ou bundlers oferecem suporte a packages prontos para uso.

A refatoração é fácil. Com as features separadas em packages, a refatoração se torna agradável. Quer refatorar o conteúdo do seu package? Vá em frente, você pode reescrevê-lo completamente, desde que mantenha a entry da API a mesma, o resto do repositório nem notará. Quer mover seu package para outro local? É só arrastar e soltar de um folder, se você não renomeá-la, o resto do repositório não será afetado. Quer renomear o package? Basta pesquisar e substituir uma string no projeto, nada mais.

Entry points explícitos. Você pode ser muito específico sobre o que exatamente de um package está disponível para os consumidores externos se quiser realmente adotar a mentalidade de "somente API pública para os consumidores". Por exemplo, você pode restringir todos os imports "profundos", tornar coisas como @project/button/some/deep/path impossíveis e forçar todos a usar somente API pública explicitamente definida no arquivo index.ts. Dê uma olhada em Package entry points e a documentação de Package exports para exemplos de como isso funciona.

Como split o código dentro de packages

A maior dificuldade das pessoas na arquitetura multi-package é qual é o momento certo para extrair código em um package? Cada pequena feature deve ser um? Ou talvez os packages sejam apenas para coisas grandes, como uma página inteira ou até mesmo um app?

Na minha experiência, há um equilíbrio aqui. Você não quer extrair cada pequena coisa em um package: você acabará com apenas uma lista plana de centenas de packages minúsculos de um único arquivo sem estrutura, o que meio que anula o propósito de introduzi-los em primeiro lugar. Ao mesmo tempo, você não gostaria que seu package se tornasse muito grande: você atingirá todos os problemas que estamos tentando resolver aqui, apenas dentro desse package.

Aqui estão alguns limites que eu costumo usar:

  • "design system" type das coisas como buttons, modal dialogs, layouts, tooltips, etc., todos devem ser packages
  • features em alguns limites de IU "naturais" são bons candidatos para um package - ou seja, algo que vive em um modal dialog, em uma drawer, em um slide-in panel, etc.
  • features "compartilháveis" - aquelas que podem ser usadas ​​em vários lugares
  • algo que você pode descrever como uma "feature" isolada com limites claros, lógicos e idealmente visíveis na IU

Além disso, assim como no artigo anterior sobre como dividir o código em components, é muito importante que um package seja responsável apenas por uma coisa conceitual. Um package que exporta um Button, CreateIssueDialog e DateTimeConverter faz muitas coisas ao mesmo tempo e precisa ser "split up".

Como organizar packages

Embora seja possível criar apenas uma lista simples de todos os packages, e para certos tipos de projetos isso funcionaria, para produtos grandes e pesados ​​de UI provavelmente não será o suficiente. Ver algo como packages "tooltip" e "settings page" juntos me faz estremecer 😖. Ou pior - se você tiver packages "backend" e "frontend" juntos. Isso não é apenas confuso, mas também perigoso: a última coisa que você quer é acidentalmente pull algum código "backend" para seu frontend bundle.

A estrutura real do repositório dependeria muito do que exatamente é o produto que você está implementando (ou mesmo quantos produtos existem), se você tem backend ou apenas frontend, e provavelmente mudará e evoluirá significativamente ao longo do tempo. Felizmente, esta é a grande vantagem dos packages: a estrutura real é completamente independente do código, você pode drag-and-drop eles e reestruturá-los uma vez por semana sem quaisquer consequências se houver necessidade.

Considerando que o custo do "erro" na estrutura é bastante baixo, não há necessidade de pensar demais, pelo menos no início. Se o seu projeto for somente frontend, você pode até começar com uma lista simples:

/packages
  /button
  ...
  /footer
  /settings
  ...

e evoluir ao longo do tempo para algo como isto:

/packages
  /core
    /button
    /modal
    /tooltip
    ...
  /product-one
    /footer
    /settings
    ...
  /product-two
    ...

Ou, se você tiver um backend, poderia ser algo assim:

/packages
  /frontend
    ... // o mesmo que acima
  /backend
    ... // alguns backend packages específicos
  /common
    ... // alguns packages que são que são conpartilhados entre frontend e backend

Onde em "common" você colocaria algum código que fosse compartilhado entre frontend e backend. Normalmente serão algumas configs, constants, utils do tipo lodash, types compartilhados.

Como estruturar um package em si

Para resumir a grande seção acima: "use monorepo, extraia features em packages". 🙂 Agora para a próxima parte - como organizar o package em si. Três coisas são importantes aqui para mim: naming convention, separar o package em layers distintas e hierarquia estrita.

Naming convention

Todo mundo adora nomear coisas e debater sobre o quão ruins são as outras coisas, não é? Para reduzir o tempo gasto em intermináveis ​​​​tópicos de comentários do GitHub e "calm down poor geeks" com TOC relacionado ao código como eu, é melhor concordar com uma "convenção de nomenclatura" de uma vez para todos.

Qual usar não importa muito na minha opinião, desde que seja seguido consistentemente ao longo do projeto. Se você tem ReactFeatureHere.ts e react-feature-here.ts no mesmo repositório, um gatinho chora em algum lugar 😿. Eu costumo usar este:

/my-feature-name
  /assets     // se eu tiver algumas imagens, elas vão para um folder próprio
    logo.svg
  index.tsx   // código principal da feature
  test.tsx    // tests para a feature se necessário
  stories.tsx // stories para storybooks se eu usar elas
  styles.(tsx|scss) // Gosto de separar os styles da lógica dos components
  types.ts    // se os types forem compartilhados entre diferentes arquivos dentro da feature
  utils.ts    // utils muito simples que são usados ​​*apenas* nesta feature
  hooks.tsx   // pequenas hooks que eu uso *somente* nesta feature

Se uma feature tiver alguns components menores imported diretamente para index.tsx, eles ficarão assim:

/my-feature-name
  ... // o mesmo que antes
  header.tsx
  header.test.tsx
  header.styles.tsx
  ... // etc

ou, mais provavelmente, eu extrairia eles imediatamente para folders e eles ficariam assim:

/my-feature-name
  ... // index o mesmo de antes
  /header
    index.tsx
    ... // etc, exatamente o mesmo naming aqui
  /footer
    index.tsx
    ... // etc, exatamente o mesmo naming aqui

A abordagem de folders é muito mais otimizada para desenvolvimento orientado a copiar e colar 😊: ao criar uma nova feature copiando e colando a estrutura da feature próxima, tudo o que você precisa fazer é renomear apenas um folder. Todos os arquivos serão nomeados exatamente da mesma forma. Além disso, é mais fácil criar um modelo mental do package, refatorar e mover o código (sobre isso na próxima seção).

Layers dentro de um package

Um package típico com uma feature complicada teria algumas "layers" distintas: pelo menos a layer "UI" e a layer "Data". Embora seja provavelmente possível misturar tudo, eu ainda recomendaria não fazer isso: renderizar buttons e buscar dados do backend são preocupações muito diferentes. Separá-los dará ao package mais estrutura e previsibilidade.

E para que o projeto permaneça relativamente saudável em termos de arquitetura e código, o crucial é ser capaz de identificar claramente as layers que são importantes para seu app, mapear o relacionamento entre elas e organizar tudo isso de uma forma que esteja alinhada com quaisquer ferramentas e frameworks usados.

Se eu estivesse implementando um projeto React do zero hoje, com Graphql para manipulações de dados e React state puro para gerenciamento de state (ou seja, sem Redux ou qualquer outra biblioteca), eu teria as seguintes layers:

  • "Data" layer - queries, mutations e outras coisas que são responsáveis ​​por conectar-se às fontes de dados externas e transformá-las. Usado apenas pela UI layer, não depende de nenhuma outra layer.
  • "Shared" layer - vários utils, functions, hooks, mini-components, types e constants que são usados ​​em todo o package por todas as outras layers. Não depende de nenhuma outra layer.
  • "ui" layer - a implementação real da feature. Depende das layers "data" e "shared", ninguém depende dela.

É isso!

diagram 1

Se eu estivesse usando alguma biblioteca externa de gerenciamento de state, provavelmente adicionaria a layer "state" também. Essa provavelmente seria uma ponte entre "data" e "ui", e portanto usaria as layers "shared" e "data" e "UI" usaria "state" em vez de "data".

diagram 2

E do ponto de vista dos detalhes da implementação, todas as layers são top-level folders em um package:

/my-feature-package
  /shared
  /ui
  /data
  index.ts
  package.json

Com cada "layer" usando a mesma naming convention descrita acima. Então sua "data" layer ficaria mais ou menos assim:

/data
  index.ts
  get-some-data.ts
  get-some-data.test.ts
  update-some-data.ts
  update-some-data.test.ts

Para packages mais complicados, eu poderia split essas layers, preservando seu propósito e suas características. A layer "Data" poderia ser split em "queries" ("getters") e "mutations" ("setters"), por exemplo, e essas podem permanecer no folder "data" ou subir:

/my-feature-package
  /shared
  /ui
  /queries
  /mutations
  index.ts
  package.json

diagram 3

Ou você pode extrair algumas sub-layers da "shared" layer, como "types" e "UI components compartilhados" (o que transformaria instantaneamente essa sub-layer no tipo "IU" a propósito, já que ninguém além de "IU" pode usar UI components).

/my-feature-package
  /shared-ui
  /ui
  /queries
  /mutations
  /types
  index.ts
  package.json

diagram 4

Contanto que você consiga definir claramente qual é o propósito de cada "sublayer", saber qual "sublayer" pertence a qual "layer" e conseguir visualizar e explicar isso para todos na equipe - tudo funciona!

Hierarquia rigorosa dentro das layers

A peça final do quebra-cabeça, que torna essa arquitetura previsível e sustentável, é uma hierarquia rigorosa dentro das layers. Isso será especialmente visível na IU layer, já que em React apps ela geralmente é a mais complicada.

Vamos começar, por exemplo, a estruturar uma página simples, com um header e um footer. Teríamos o arquivo "index.ts" - o arquivo principal, onde a página se reúne, e os components "header.ts" e "footer.ts".

/my-page
  index.ts
  header.ts
  footer.ts

Agora, todos eles terão seus próprios components que eu gostaria de colocar em seus próprios arquivos. "Header", por exemplo, terá os components "Search bar" e "Send feedback". Na maneira flat "tradicional" de organizar apps, nós colocaríamos eles um ao lado do outro, não é? Seria algo assim:

/my-page
  index.ts
  header.ts
  footer.ts
  search-bar.ts
  send-feedback.ts

E então, se eu quiser adicionar o mesmo button "send-feedback" ao footer component, eu novamente faria import dele para "footer.ts" de "send-feedback.ts", certo? Afinal, ele está próximo e parece natural.

diagram 5

Infelizmente, o que aconteceu foi que violamos os limites entre nossas layers ("UI" e "shared") sem nem perceber. Se eu continuasse adicionando mais e mais components a essa estrutura flat, e provavelmente continuarei, applications reais tendem a ser bem complicadas, provavelmente violarei elas mais algumas vezes. Isso transformará esse folder em sua própria "bola de lama", onde é completamente imprevisível qual component depende de qual. E, como resultado, desembaraçar tudo isso e extrair algo desse folder, quando chegar a hora da refatoração, pode se tornar um exercício muito complicado.

Em vez disso, podemos estruturar essa layer de forma hierárquica. As regras são:

  • apenas os arquivos principais (por exemplo, "index.ts") em uma pasta podem ter subcomponents (sub-modules) e podem import eles
  • você pode import apenas dos "children", não dos "vizinhos"
  • você não pode pular um level e pode import apenas dos children diretos

Ou, se preferir visual, é apenas uma árvore:

tree

E se você precisar compartilhar algum código entre diferentes levels dessa hierarquia (como nosso send-feedback component), você verá instantaneamente que está violando as regras da hierarquia, já que onde quer que você coloque ele, você terá que import ele dos parents ou dos vizinhos. Então, em vez disso, ele seria extraído para a layer "shared" e imported de lá.

diagram 6

Ficaria assim:

/my-page
  /shared
    send-feedback.ts
  /ui
    index.ts
    /header
      index.ts
      search-bar.ts
    /footer
      index.ts

Dessa forma, a IU layer (ou qualquer layer onde essa regra se aplica) se transforma em uma estrutura de árvore, onde cada branch é independente de qualquer outro branch. Extrair qualquer coisa desse package agora é moleza: tudo o que você precisa fazer é drag e drop um folder em um novo lugar. E você sabe com certeza que nenhum component na árvore da IU será afetado por ela, exceto aquele que realmente usa ele. A única coisa com a qual você pode precisar lidar adicionalmente é a "shared" layer.

O app completo com a data layer ficaria assim:

diagram 7

Algumas layers claramente definidas, completamente encapsuladas e previsíveis.

/my-page
  /shared
    send-feedback.ts
  /data
    get-something.ts
    send-something.ts
  /ui
    index.ts
    /header
      index.ts
      search-bar.ts
    /footer
      index.ts

React recomenda não aninhar

Se você ler a documentação do React sobre a estrutura de projeto recomendada, verá que React realmente recomenda não aninhar muito. A recomendação oficial é "considere limitar-se a um máximo de três ou quatro pastas aninhadas dentro de um único projeto". E essa recomendação é muito relevante para essa abordagem também: se seu package ficar muito aninhado, é um sinal claro de que você pode precisar pensar em splitting ele em packages menores. 3-4 níveis de aninhamento, na minha experiência, são suficientes até mesmo para features muito complicadas.

A beleza da arquitetura de packages, porém, é que você pode organizar seus packages com tanto aninhamento quanto precisar sem ficar preso a essa restrição - você nunca se refere a outro package por seu relative path, apenas por seu nome. Um package com o nome @project/change-setting-dialog que reside no path packages/change-settings-dialog ou está oculto dentro de /packages/product/features/settings-page/change-setting-dialog, será chamado de @project/change-setting-dialog independentemente de sua localização física.

Ferramenta de gerenciamento Monorepo

É impossível falar sobre multi-packages monorepo para sua arquitetura sem tocar pelo menos um pouco nas ferramentas de gerenciamento monorepo. O maior problema geralmente é o gerenciamento de dependências dentro dele. Imagine se alguns dos seus monorepo packages usam uma dependência externa, lodash por exemplo.

/my-feature-one
  package.json // este usa [email protected]
/my-other-feature
  package.json // este usa [email protected]

Agora o lodash lança uma nova versão, [email protected], e você quer mover seu projeto para ele. Você precisaria atualizá-lo em todos os lugares ao mesmo tempo: a última coisa que você quer é que alguns dos packages permaneçam na versão antiga, enquanto outros usem a nova. Se você estiver no npm ou no antigo yarn, isso seria um desastre: eles instalariam várias cópias (não duas, várias) do lodash em seu sistema, o que resultaria em tempos maiores de instalação e build, e seus tamanhos de bundles disparando. Sem mencionar a diversão de desenvolver uma nova feature quando você está usando duas versões diferentes da mesma biblioteca em todo o projeto.

Não vou tocar no que usar se seu projeto for publicado no npm e de código aberto: provavelmente algo como Lerna seria o suficiente, mas esse é um tópico completamente diferente.

Se, no entanto, seu repositório for private, as coisas estão ficando mais interessantes. Porque tudo o que você realmente precisa para que essa arquitetura funcione é packages "aliasing", nada mais. Ou seja, apenas links simbólicos básicos que tanto o Yarn quanto o Npm fornecem por meio da ideia de workspaces. Parece com isso. Você tem o arquivo "root" package.json, onde você declara onde estão os workspaces (ou seja, seus packages locais):

{
    "private": true,
    "workspaces": ["packages/**"]
}

E então, da próxima vez que você executar yarn install, todos os packages do folder packages se tornarão packages "adequados" e estarão disponíveis no seu projeto por meio dos name deles. Esse é todo o monorepo setup!

Quanto às dependências. O que acontecerá se você tiver a mesma dependência em alguns packages?

/packages
  /my-feature-one
    package.json // este usa [email protected]
  /my-other-feature
    package.json // este usa [email protected]

Quando você executa yarn install, ele irá "hoist" (içar) esse package para o root node_modules:

/node_modules
  [email protected]
/packages
  /my-feature-one
    package.json // este usa [email protected]
  /my-other-feature
    package.json // este usa [email protected]

Esta é exatamente a mesma situação que se você declarasse [email protected] apenas no root package.json. O que estou dizendo é, e provavelmente serei enterrado vivo pelos puristas da internet por isso, incluindo eu mesmo há dois anos: você não precisa declarar nenhuma das dependências em seus packages locais. Tudo pode ir para o root package.json. E seus arquivos package.json em packages locais serão apenas arquivos json muito leves, que especificam apenas os fields "name" e "main".

Configuração muito mais fácil de gerenciar, especialmente se você estiver apenas começando.

Estrutura do projeto React para scale: visão geral final

Huh, isso foi muito texto. E mesmo isso é apenas uma breve visão geral: muitas outras coisas podem ser ditas sobre o tópico! Vamos recapitular o que já foi dito, pelo menos:

Decomposition é a chave para scale com sucesso seu React app. Pense no seu projeto não como um "projeto" monolítico, mas como uma combinação de "features" independentes do tipo caixa-preta com sua própria API pública para os consumidores usarem. A mesma discussão de "monólito" vs "microservices" realmente.

Arquitetura Monorepo é perfeita para isso. Extraia suas features em packages; organize seus packages da maneira que melhor funciona para seu projeto.

Layers dentro de um package são importantes para dar a ele alguma estrutura. Você provavelmente terá pelo menos uma "data" layer, uma "UI" layer e uma "shared" layer. Pode introduzir mais, dependendo de suas necessidades, só precisa ter limites claros entre elas.

Estrutura hierárquica de um package é legal. Ela facilita a refatoração, força você a ter limites mais claros entre as layers e força você a dividir seu package em outros menores quando ele se torna muito grande.

Gerenciamento de dependências em um monorepo é um tópico complicado, mas se seu projeto for private, você não precisa se preocupar com isso. Apenas declare todas as suas dependências no root package.json e mantenha todos os packages locais livres delas.

Você pode dar uma olhada na implementação dessa arquitetura neste repositório de exemplo: https://github.com/developerway/example-react-project. Este é apenas um exemplo básico para demonstrar os princípios descritos no artigo, então não se assuste com packages pequenos com apenas um index.ts: em um app real, eles serão muito maiores.

Isso é tudo por hoje. Espero que você consiga aplicar alguns desses princípios (ou até mesmo todos eles!) aos seus apps e veja melhorias no seu desenvolvimento diário imediatamente! ✌🏼

Fonte

Artigo escrito por Nadia Makarevich.