Articolo originale: A Better Way to Structure React Projects

Ciao a tutti! Molte parole sono già state spese a proposito di argomenti relativamente facili come "Fare X in React" o "Usare React con la tecnologia X".

In questo articolo, invece, voglio parlare delle esperienze che ho avuto costruendo front end da zero presso Delightchat e precedentemente in altre aziende.

Questi progetti richiedono una comprensione più profonda di React e un prolungato utilizzo in un contesto di produzione.

Introduzione

In breve, un progetto React complesso dovrebbe essere strutturato in questo modo. Sebbene io utilizzi NextJS in produzione, questa struttura di file dovrebbe essere abbastanza utile in qualsiasi impostazione di React.

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

Nota: nella struttura di file qui sopra, le risorse o i file statici dovrebbero essere inseriti in una qualunque variante della cartella public per il proprio framework.

Passiamo a esaminare le cartelle qui sopra in ordine.

1. Adattatori

Gli adattatori (adapter) sono i connettori di una applicazione con il mondo esterno. Qualsiasi forma di chiamata API o interazione websocket necessaria per condividere dati con un servizio esterno o un client dovrebbe verificarsi all'interno dell'adattatore stesso.

Ci sono casi in cui alcuni dati sono sempre condivisi tra tutti gli adattatori – ad esempio, la condivisione di cookie, URL base e header su tutti gli adattatori AJAX (XHR). Questi possono essere inizializzati nella cartella xhr, quindi importati all'interno di altri adattatori da usare successivamente.

La struttura avrà questo aspetto:

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

Nel caso di axios, puoi usare axios.create per creare un adattatore base ed esportare questa istanza inizializzata, oppure creare funzioni diverse per get, post, patch e delete per una ulteriore astrazione. Qualcosa di questo tipo:

// 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 così via ...

Dopo che il file di base (o i file) è pronto, crea un file adattatore separato per ciascuna pagina, o ciascun insieme di funzionalità, a seconda della complessità dell'applicazione. Una funzione con un nome descrittivo rende molto facile comprendere ciò che fa ciascuna chiamata API e quello che dovrebbe ottenere.

// adapters/page1Adapter/index.tsx

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

// funzioni con nome descrittivo
export function getData(){
  return get(someUrl);
}

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

... e così via ...

In che modo gli adattatori possono essere utili? Scopriamolo nella prossima sezione.

2. Componenti

Sebbene in questa sezione dovremmo parlare dei contesti, vorrei parlare prima dei componenti. Questo per capire il motivo per cui è richiesto (e necessario) un contesto in applicazioni complesse.

I componenti (component) sono la linfa vitale di un'applicazione. Gestiscono l'interfaccia utente (UI) dell'applicazione e possono talvolta contenere la logica di business e anche qualsiasi stato che debba essere mantenuto.

Nel caso in cui un componente diventi troppo complesso per esprimere una logica di business con una interfaccia utente, è meglio essere in grado di dividerlo in un file bl.tsx separato, con la radice index.tsx che importa tutte le funzioni e i gestori da esso.

La struttura dovrebbe essere simile a questa:

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

In questa struttura, ogni pagina è contenuta nella propria cartella all'interno di components, in modo che sia agevole scoprire su cosa influisce ogni componente.

È anche importante limitare l'ambito di azione di un componente. Vale a dire che un componente dovrebbe usare solo adattatori per il recupero dei dati, avere un file separato per logiche di business complesse, e focalizzarsi solo sulla parte dell'interfaccia utente.

// components/page1Components/Component1/index.tsx

import businessLogic from "./bl.tsx";

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

  return {
    // JSX
  }
}

Mentre il file con la logica di business importa solo i dati e li restituisce:

// 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);
  }, [])
}

Tuttavia, c'è un problema che è comune nell'ambito di tutte le applicazioni complesse. La gestione dello stato e come condividere lo stato attraverso componenti distanti fra loro. Per esempio, consideriamo la seguente struttura di file:

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

Se occorre condividere un qualche stato attraverso ComponentA e ComponentB, nell'esempio qui sopra, esso dovrebbe essere passato attraverso tutti i componenti intermedi, e anche a qualsiasi altro componente che voglia interagire con lo stato.

Per risolvere ciò ci sono parecchie soluzioni che possono essere usate, come Redux, Easy-Peasy e React Context, ognuna della quali ha i propri pro e contro. In generale, React Context dovrebbe essere "sufficientemente valido" per risolvere questo problema. Tutti i file relativi al contesto vengono conservati in contexts.

3. Contesti

La cartella contexts è una cartella minimale che contiene solo lo stato che deve essere condiviso tra questi componenti. Ciascuna pagina può avere parecchi contesti annidati, ciascuno dei quali fa solamente da tramite per passare i dati verso il basso nella gerarchia. Tuttavia, per evitare complessità, è meglio avere un singolo file di contesto. La struttura dovrebbe essere tipo questa:

contexts
|---page1Context
        |---index.tsx (Esporta consumer, provider, ...)
        |---Context1.tsx (Contiene parte dello stato)
        |---Context2.tsx (Contiene parte dello stato)
|---page2Context
        |---index.tsx (Abbastanza semplice da avere anche lo stato)

Nel caso qui sopra, visto che page1 potrebbe essere un poco più complessa, è consentito un qualche contesto annidato passando al genitore il contesto figlio. Tuttavia, generalmente, un singolo file index.tsx che contenga lo stato ed esporti i file rilevanti dovrebbe essere sufficiente.

Non approfondirò la parte relativa all'implementazione delle librerie relative alla gestione dello stato di React, visto che ognuna di esse è distinta dalle altre e ha i propri vantaggi e svantaggi. Di conseguenza, raccomando di seguire il tutorial di qualunque cosa decidi di usare per impararne le migliori pratiche.

Al contesto è consentito importare dagli adattatori per recuperare dati e reagire a effetti esterni. Nel caso di React Context, i provider sono importati all'interno delle pagine per condividere lo stato tra tutti i componenti, usando qualcosa tipo useContext all'interno di questi componenti per riuscire a utilizzare i dati.

E ora passiamo all'ultimo grande pezzo del puzzle, le pagine.

4. Pagine

Voglio evitare di essere di parte rispetto a un framework per questo articolo, ma in generale, avere una cartella specifica nella quale piazzare componenti a livello di rotta è una buona pratica.

Gatsby e NextJS obbligano ad avere tutte le rotte in una cartella chiamata pages. Questo è un modo piuttosto leggibile per definire componenti a livello di rotta, e mimando questo in una applicazione generata con CRA (create-react-app) si ha come risultato anche una migliore leggibilità del codice.

Un punto centralizzato per le rotte aiuta anche all'utilizzo della funzionalità "Vai Al File" della maggior parte degli ambienti di sviluppo integrati (IDE) per accedere a un file usando Cmd (o Ctrl) + Click su un'istruzione di import.

Questo aiuta a spostarsi attraverso il codice velocemente, avendo chiara l'appartenenza di ogni cosa. Imposta anche una chiara gerarchia di differenziazione tra pagine e componenti, laddove una pagina può importare un componente per visualizzarlo senza fare altro, neppure logica di business.

In ogni caso, è possibile importare fornitori di contesto (context provider) all'interno di una pagina, in modo che i componenti figli li possano utilizzarli. Oppure, nel caso di NextJS, scrivere del codice lato server in grado di passare dati ai componenti usando getServerSideProps o getStaticProps.

5. Stili

Infine, eccoci agli stili. Nonostante il mio metodo sia di incorporare semplicemente gli stili all'interno dell'interfaccia utente usando una soluzione CSS-in-JS come gli styled-component, a volte è utile avere un insieme globale di stili in un file CSS.

Un puro e semplice file CSS è più condivisibile tra i progetti e può anche agire sul  CSS di componenti, laddove non possono arrivare gli styled-component (ad esempio componenti di terze parti).

Quindi, è possibile conservare tutti questi file CSS all'interno di una cartella  styles e importarli o linkarli liberamente da qualsiasi parte desideri.