Artigo original: A Better Way to Structure React Projects

Olá, pessoal! Já derramamos muita tinta (eletrônica) em assuntos relativamente fáceis, como "Fazer X em React" ou "Usar React com a tecnologia X".

Em vez disso, eu gostaria de falar sobre experiências que tive criando front-ends do início na DelightChat e nas empresas em que trabalhei antes.

Esses projetos exigem um entendimento mais profundo de React e do uso estendido em um ambiente de produção.

Se quiser assistir um vídeo desse tutorial para complementar sua leitura, pode fazer isso aqui (vídeo em inglês).

Introdução

Resumidamente, um projeto complexo de React deve ser estruturado assim. Embora eu use NextJS em produção, esta estrutura de arquivos deve ser bastante útil em qualquer ambiente de React.

src
|---adapters
|---contexts
|---components
|---styles
|---pages

Observação: na estrutura de arquivos acima, os assets ou arquivos estáticos devem ser colocados em qualquer variação da pasta public *na qual o seu framework estiver.*

Discutiremos cada uma das pastas acima em ordem de precedência.

1. Adaptadores

Adapters são os conectores de sua aplicação com o mundo exterior. Qualquer forma de chamada de API ou interação com websocket que precise acontecer, para compartilhar dados com um serviço ou client externo, deve acontecer dentro do próprio adaptador.

Esses são casos onde alguns dados são sempre compartilhados entre todos os adaptadores – por exemplo, compartilhamento de cookies, URL de base e cabeçalhos entre seus adaptadores AJAX (XHR). Eles podem ser inicializados na pasta xhr, sendo importados dentro de seus adaptadores para serem usados posteriormente.

A estrutura terá a seguinte aparência:

adapters
|---xhr
|---page1Adapter
|---page2Adapter

No caso do axios, você pode usar axios.create para criar um adaptador de base, e depois exportar essa instância inicializada u criar funções diferentes para get, post, patch e delete para abstrai-los ainda mais. Esta será a aparência:

// adapters/xhr/index.tsx

import Axios from "axios";

function returnAxiosInstance() {
  return Axios.create(initializers);
}

export function get(url){
  const axios = returnAxiosInstance();
  return axios.get(url);
}

export function post(url, requestData){
  const axios = returnAxiosInstance();
  return axios.post(url, requestData);
}

... e assim por diante ...

Depois de ter seu arquivo (ou arquivos) de base prontos, crie um arquivo adaptador separado para cada página ou para cada conjunto de funcionalidades, dependendo da complexidade do seu app. Uma função bem nomeada torna fácil entender o que cada chamada de API faz e o que ela deve conseguir.

// adapters/page1Adapter/index.tsx

import { get, post } from "adapters/xhr";
import socket from "socketio";

// well-named functions
export function getData(){
  return get(someUrl);
}

export function setData(requestData){
  return post(someUrl, requestData);
}

... e assim por diante ...

Mas qual será a utilidade desses adaptadores? Descobriremos na próxima seção.

2. Componentes

Embora precisemos falar de contextos nesta seção, eu gostaria de falar primeiramente dos componentes. Isso é para entender por que o contexto é obrigatório (e necessário) em aplicações complexas.

Components são o motor de sua aplicação. Eles terão a UI da sua aplicação, e por vezes a lógica de negócios, além de qualquer state que precise ser mantido.

Caso um componente se torne muito complexo para expressar a lógica de negócios com sua UI, é bom ser capaz de dividi-lo em um arquivo bl.tsx (o arquivo de lógica de negócios) separado, com seu arquivo raiz (root) index.tsx importando todas as funções e manipuladores dele.

A estrutura tem esta aparência:

components
|---page1Components
        |--Component1
        |--Component2
|---page2Component
        |--Component1
               |---index.tsx
               |---bl.tsx

Nessa estrutura, cada página obtém sua própria pasta dentro dos componentes. Desse modo, é fácil saber qual componente afeta o quê.

Também é importante limitar o escopo de um componente. Assim, um componente somente deve usar adapters para buscar dados, ter um arquivo separado para a lógica de negócios complexa, e somente se concentrar na parte da UI.

// components/page1Components/Component1/index.tsx

import businessLogic from "./bl.tsx";

export default function Component2() {
  
  const { state and functions } = businessLogic();

  return {
    // JSX
  }
}

Enquanto isso, o arquivo da lógica de negócios somente importe os dados e os retorne it:

// components/page1Components/Component1/bl.tsx

import React, {useState, useEffect} from "react";
import { adapters } from "adapters/path_to_adapter";

export default function Component1Bl(){
  const [state, setState] = useState(initialState);

  useEffect(() => {
    fetchDataFromAdapter().then(updateState);
  }, [])
}

No entanto, há um problema comum em todos os apps complexos. O gerenciamento do state, e como compartilhar o state entre componentes distantes. Por exemplo, considerar a seguinte estrutura de dados:

components
|---page1Components
        |--Component1
               |---ComponentA
|---page2Component
        |--ComponentB

Se algum state precisar ser compartilhado entre ComponentA e ComponentB no exemplo acima, ele precisará ser passado por todos os componentes intermediários, bem como por qualquer outro componente que deseje interagir com o state.

Para resolver isso, existem várias soluções que podem ser usadas, como o Redux, o Easy-Peasy e o React Context, cada um deles tendo seus prós e contras. Em geral, o React Context deve ser "suficientemente bom" para resolver esse problema. Armazenamos todos os arquivos relacionados ao contexto em contexts.

3. Contextos

A pasta contexts é uma pasta com o mínimo possível, contendo apenas o state que precisa ser compartilhado entre esses componentes. Cada página pode ter vários contextos aninhados, com cada contexto passando apenas os dados adiante, de cima para baixo. Porém, para evitar a complexidade, é melhor ter apenas um único arquivo de contexto. A estrutura terá a seguinte aparência:

contexts
|---page1Context
        |---index.tsx (Exporta os consumidores, fornecedores, ...)
        |---Context1.tsx (Contém parte do state)
        |---Context2.tsx (Contém parte do state)
|---page2Context
        |---index.tsx (Suficientemente simples para também ter um state)

No caso acima, como page1 pode ser um pouco mais complexa, permitimos alguns contextos aninhados passando o contexto do filho com um filho para o elemento pai. No entanto, em geral, um único arquivo index.tsx contendo um state e exportando os arquivos relevantes deve ser o suficiente.

Não entrarei na parte da implementação das bibliotecas de gerenciamento de state do React, já que cada uma delas tem sua própria complexidade, lados positivos e negativos. Recomendo, portanto, ver um tutorial da biblioteca que você decidir utilizar para aprender as melhores práticas.

É permitido ao contexto importar dos adapters para fazer o fetch e para reagir aos efeitos externos. No caso do React Context, os fornecedores são importados dentro das páginas para compartilhar o state entre todos os componentes, e algo como useContext é usado dentro desses components para conseguir utilizar esses dados.

Vamos agora para a última parte maior desse quebra-cabeças, pages.

4. Páginas

Quero evitar ser tendencioso com relação a um framework para esta parte. Em geral, no entanto, uma pasta específica para os componentes em nível de rota (route) deve ser usada para fins de boas práticas.

O Gatsby e o NextJS exigem que todas as rotas estejam em uma pasta chamada pages. Esta é uma maneira bastante legível de definir componentes em nível de rota e de copiar isso em sua aplicação gerada pelo create-react-app, resultando, também, em melhor legibilidade do código.

Um local centralizado para rotas também ajuda você a utilizar a funcionalidade "Go To File" (Ir para o arquivo) da maioria das IDEs de saltar para um arquivo usando Cmd ou Ctrl + clique em um import.

Isso ajuda você a passar pelo código de modo rápido e com clareza sobre o que vai em cada lugar. Também define uma hierarquia clara de diferenciação entre pages e components, onde uma página pode importar um componente para exibi-la e faze apenas isso, nem mesmo a lógica de negócios.

Porém, é possível importar fornecedores de contexto dentro de sua página para que os componentes filhos possam consumi-los. Ou, no caso do NextJS, escrever algum código do lado do servidor que possa passar dados para seus componentes usando getServerSideProps ou getStaticProps.

5. Estilos

Por fim, temos os estilos. Embora minha forma preferencial seja a de incorporar os estilos dentro da UI usando uma solução do tipo CSS-no-JS, como o styled-components, às vezes, é útil ter um conjunto global de estilos em um arquivo CSS.

O bom e velho arquivo CSS é mais compartilhável entre os projetos, podendo, também, afetar o CSS dos componentes que o styled-components não consegue (por exemplo, componentes de terceiros).

Assim, você pode armazenar todos esses arquivos CSS dentro da pasta styles e importá-los ou fazer um link para eles livremente do local onde você desejar.

Essas eram as minhas ideias. Fiquem à vontade para enviar um e-mail para o autor caso queiram discutir algo ou se tiverem mais a compartilhar sobre maneiras de melhorar esse processo!

Para outras atualizações ou discussões, sigam o autor no Twitter aqui.

Outro artigo do autor escrito para o freeCodeCamp foi sobre como começar com o Deno criando um redutor de URL. Você pode ler esse artigo aqui (em inglês).