In che modo la "Regola d'oro" dei componenti di React può aiutarti a scrivere codice migliore

E come entrano in gioco gli ami

Di recente ho adottato una nuova filosofia che cambia il modo in cui costruisco i componenti. Non è necessariamente una nuova idea, ma piuttosto un nuovo modo di pensare sottile.

La regola d'oro dei componenti

Crea e definisci i componenti nel modo più naturale, considerando esclusivamente ciò di cui hanno bisogno per funzionare.

Ancora una volta, è un'affermazione sottile e potresti pensare di seguirla già, ma è facile andare contro questo.

Ad esempio, supponiamo che tu abbia il seguente componente:

PersonCard

Se avessi definito questo componente in modo "naturale", probabilmente lo avresti scritto con la seguente API:

Il che è piuttosto semplice: solo guardando a ciò che deve funzionare, hai solo bisogno di un nome, titolo di lavoro e URL dell'immagine.

Supponiamo che tu abbia l'obbligo di mostrare un'immagine "ufficiale" in base alle impostazioni dell'utente. Potresti essere tentato di scrivere un'API in questo modo:

Può sembrare che il componente abbia bisogno di quegli oggetti di scena aggiuntivi per funzionare, ma in realtà il componente non ha un aspetto diverso e non ha bisogno di quegli oggetti di scena aggiuntivi per funzionare. Quello che fanno questi oggetti di scena extra è accoppiare questa impostazione ufficiale preferita al componente e rendere qualsiasi uso del componente al di fuori di quel contesto sembra davvero innaturale.

Colmare il gap

Quindi, se la logica per cambiare l'URL dell'immagine non appartiene al componente stesso, dove appartiene?

Che ne dici di un file indice?

Abbiamo adottato una struttura di cartelle in cui ogni componente viene inserito in una cartella omonima in cui il file di indice è responsabile per colmare il divario tra il componente "naturale" e il mondo esterno. Questo file viene chiamato "contenitore" (ispirato al concetto di "contenitore" di React Redux).

/ PersonCard
  -PersonCard.js ------ il componente "naturale"
  -index.js ----------- il "contenitore"

Definiamo i contenitori come il pezzo di codice che colma il divario tra il tuo componente naturale e il mondo esterno. Per questo motivo, a volte chiamiamo anche queste cose "iniettori".

Il tuo componente naturale è il codice che creeresti se ti venisse mostrata un'immagine di ciò che ti era richiesto (senza i dettagli di come avresti ottenuto i dati o dove sarebbero stati inseriti nell'app - tutto quello che sai è che dovrebbe funzionare).

Il mondo esterno è una parola chiave che useremo per fare riferimento a qualsiasi risorsa della tua app (ad esempio il negozio Redux) che può essere trasformata per soddisfare le caratteristiche del tuo componente naturale.

Obiettivo di questo articolo: come possiamo mantenere i componenti "naturali" senza inquinarli con la spazzatura proveniente dal mondo esterno? Perché è meglio?

Nota: sebbene ispirato dalla terminologia di Dan Abramov e React Redux, la nostra definizione di "contenitori" va leggermente oltre ed è leggermente diversa.
L'unica differenza tra il contenitore di Dan Abramov e il nostro è solo a livello concettuale. Dan afferma che esistono due tipi di componenti: componenti di presentazione e componenti del contenitore. Facciamo un ulteriore passo avanti e diciamo che ci sono componenti e quindi contenitori.
Anche se implementiamo contenitori con componenti, non pensiamo ai contenitori come componenti a livello concettuale. Ecco perché ti consigliamo di inserire il tuo contenitore nel file indice, perché è un ponte tra il tuo componente naturale e il mondo esterno e non si regge da solo.

Sebbene questo articolo sia incentrato sui componenti, i contenitori occupano la maggior parte di questo articolo.

Perché?

Realizzare componenti naturali - Facile, anche divertente.
Collegamento dei componenti al mondo esterno - Un po 'più difficile.

Per come la vedo io, ci sono tre ragioni principali per cui inquini il tuo componente naturale con spazzatura proveniente dal mondo esterno:

  1. Strane strutture di dati
  2. Requisiti al di fuori dell'ambito del componente (come nell'esempio sopra)
  3. Eventi di lancio su aggiornamenti o su mount

Le prossime sezioni proveranno a coprire queste situazioni con esempi con diversi tipi di implementazioni di container.

Lavorare con strane strutture di dati

A volte per rendere le informazioni richieste, è necessario collegare insieme i dati e trasformarli in qualcosa di più sensato. Per mancanza di una parola migliore, le strutture di dati "strane" sono semplicemente strutture di dati che sono innaturali da usare per il componente.

È molto allettante passare direttamente strane strutture di dati in un componente e fare la trasformazione all'interno del componente stesso, ma questo porta a componenti confusi e spesso difficili da testare.

Mi sono sorpreso a cadere in questa trappola di recente quando mi è stato assegnato il compito di creare un componente che ha ottenuto i suoi dati da una particolare struttura di dati che utilizziamo per supportare un particolare tipo di modulo.

Il componente stesso

Il componente ha assunto questa strana struttura di dati di campo come prop. In pratica, questo potrebbe andare bene se non avessimo mai dovuto toccare di nuovo la cosa, ma è diventato un vero problema quando ci è stato chiesto di usarla di nuovo in un punto diverso non correlato a questa struttura di dati.

Dato che il componente richiedeva questa struttura di dati, era impossibile riutilizzarlo ed era confuso il refactoring. Anche i test che abbiamo scritto inizialmente erano confusi perché hanno deriso questa strana struttura di dati. Abbiamo avuto difficoltà a comprendere i test e problemi a riscriverli quando alla fine abbiamo eseguito il refactoring.

Sfortunatamente, sono inevitabili strane strutture di dati, ma usare i contenitori è un ottimo modo per gestirle. Uno da asporto qui è che l'architettura dei componenti in questo modo ti dà la possibilità di estrarre e classificare il componente in uno riutilizzabile. Se si passa una strana struttura dati in un componente, si perde quell'opzione.

Nota: non sto suggerendo che tutti i componenti creati siano generici dall'inizio. Il suggerimento è di pensare a cosa fa il componente a un livello fondamentale e quindi colmare il divario. Di conseguenza, è più probabile che tu abbia la possibilità di convertire il componente in uno riutilizzabile con un lavoro minimo.

Implementazione di contenitori mediante componenti di funzione

Se stai rigorosamente mappando oggetti di scena, una semplice opzione di implementazione è quella di utilizzare un altro componente di funzione:

E la struttura delle cartelle per un componente come questo assomiglia a:

/ ChipField
  -ChipField.js ------------------ il campo chip "naturale"
  -ChipField.test.js
  -index.js ---------------------- il "contenitore"
  -index.test.js
  / helpers ----------------------- una cartella per helpers / utils
    -getValuesFromField.js
    -getValuesFromField.test.js
    -transformValuesToField.js
    -transformValuesToField.test.js

Potresti pensare "che è troppo lavoro" - e se lo sei, lo capisco. Potrebbe sembrare che ci sia più lavoro da fare qui poiché ci sono più file e un po 'di riferimento indiretto, ma ecco la parte che ti manca:

È sempre la stessa quantità di lavoro, indipendentemente dal fatto che tu abbia trasformato i dati all'esterno del componente o all'interno del componente. La differenza è che, quando trasformi i dati al di fuori del componente, ti stai dando un punto più esplicito per testare che le tue trasformazioni sono corrette, pur separando le preoccupazioni.

Soddisfare i requisiti al di fuori dell'ambito del componente

Come nell'esempio della Carta della persona sopra, è molto probabile che quando adotti questa "regola d'oro" del pensiero, ti renderai conto che determinati requisiti sono al di fuori dell'ambito del componente reale. Quindi, come riesci a soddisfarli?

Hai indovinato: contenitori

Puoi creare contenitori che svolgono un po 'di lavoro extra per mantenere naturale il tuo componente. Quando lo fai, ti ritrovi con un componente più focalizzato che è molto più semplice e un contenitore che viene testato meglio.

Implementiamo un contenitore PersonCard per illustrare l'esempio.

Implementazione di container utilizzando componenti di ordine superiore

React Redux utilizza componenti di ordine superiore per implementare contenitori che spingono e mappano oggetti di scena dal negozio Redux. Da quando abbiamo ottenuto questa terminologia da React Redux, non sorprende che la connessione di React Redux sia un contenitore.

Indipendentemente se si utilizza un componente di funzione per mappare oggetti di scena o se si utilizzano componenti di ordine superiore per connettersi al negozio Redux, la regola d'oro e il lavoro del contenitore sono sempre gli stessi. Innanzitutto, scrivi il componente naturale e quindi utilizza il componente di ordine superiore per colmare il divario.

Struttura delle cartelle per sopra:

/ PersonCard
  -PersonCard.js ----------------- componente naturale
  -PersonCard.test.js
  -index.js ---------------------- contenitore
  -index.test.js
  / aiutanti
    -getPictureUrl.js ------------ helper
    -getPictureUrl.test.js
Nota: in questo caso, non sarebbe troppo pratico avere un aiuto per getPictureUrl. Questa logica è stata separata semplicemente per dimostrare che è possibile. Potresti anche aver notato che non vi sono differenze nella struttura delle cartelle indipendentemente dall'implementazione del contenitore.

Se hai già usato Redux in precedenza, l'esempio sopra è qualcosa che probabilmente conosci già. Ancora una volta, questa regola d'oro non è necessariamente una nuova idea ma un nuovo modo di pensare sottile.

Inoltre, quando si implementano contenitori con componenti di ordine superiore, si ha anche la possibilità di comporre insieme componenti di ordine superiore funzionalmente - passando oggetti di scena da un componente di ordine superiore a quello successivo. Storicamente, abbiamo messo insieme più componenti di ordine superiore per implementare un singolo contenitore.

Nota del 2019: la comunità React sembra allontanarsi dai componenti di ordine superiore come modello.
Vorrei anche raccomandare lo stesso. La mia esperienza quando si lavora con questi è che possono essere fonte di confusione per i membri del team che non hanno familiarità con la composizione funzionale e possono causare ciò che è noto come "inferno dell'involucro" in cui i componenti vengono avvolti troppe volte causando problemi significativi di prestazioni.
Ecco alcuni articoli e risorse su questo: Hooks talk (2018) Ricomponi talk (2016), Use a Render Prop! (2017), Quando NON usare Render Props (2018).

Mi hai promesso dei ganci

Implementazione di contenitori mediante ganci

Perché gli hook sono presenti in questo articolo? Perché l'implementazione dei contenitori diventa molto più semplice con i ganci.

Se non hai familiarità con i ganci di React, allora consiglierei di guardare i discorsi di Dan Abramov e Ryan Florence che introducono il concetto durante React Conf 2018.

L'essenza è che gli hook sono la risposta del team React ai problemi con componenti di ordine superiore e schemi simili. I ganci React sono pensati per essere un modello di sostituzione superiore per entrambi nella maggior parte dei casi.

Ciò significa che l'implementazione dei contenitori può essere eseguita con un componente di funzione e ganci

Nell'esempio seguente, stiamo usando gli hook useRoute e useRedux per rappresentare il "mondo esterno" e stiamo usando l'helper getValues ​​per mappare il mondo esterno in oggetti di scena utilizzabili dal tuo componente naturale. Stiamo anche utilizzando l'helper transformValues ​​per trasformare l'output del tuo componente nel mondo esterno rappresentato dall'invio.

Ed ecco la struttura delle cartelle di riferimento:

/ FooComponent ----------- l'intero componente che altri possono importare
  -FooComponent.js ------ la parte "naturale" del componente
  -FooComponent.test.js
  -index.js ------------- il "contenitore" che colma il divario
  -index.js.test.js e fornisce dipendenze
  / helpers -------------- helper isolati che puoi testare facilmente
    -getValues.js
    -getValues.test.js
    -transformValues.js
    -transformValues.test.js

Eventi di cottura in container

L'ultimo tipo di scenario in cui mi trovo a divergere da un componente naturale è quando devo sparare eventi legati al cambio di oggetti di scena o componenti di montaggio.

Ad esempio, supponiamo che tu abbia il compito di creare una dashboard. Il team di progettazione ti consegna un modello del cruscotto e lo trasforma in un componente React. Ora sei nel punto in cui devi popolare questa dashboard con i dati.

Si nota che è necessario chiamare una funzione (ad esempio dispatch (fetchAction)) quando il componente viene montato affinché ciò avvenga.

In scenari come questo, mi sono ritrovato ad aggiungere i metodi del ciclo di vita componentDidMount e componentDidUpdate e l'aggiunta di puntelli onMount o onDashboardIdChanged perché avevo bisogno di un evento da attivare per collegare il mio componente al mondo esterno.

Seguendo la regola d'oro, questi oggetti di scena onMount e onDashboardIdChanged sono innaturali e quindi dovrebbero vivere nel container.

La cosa bella degli hook è che rende molto più semplice l'invio di eventi su Mount o sul cambio prop!

Eventi di tiro sul monte:

Per generare un evento sul mount, chiama useEffect con un array vuoto.

Eventi di lancio su cambi di prop:

useEffect ha la capacità di controllare la tua proprietà tra i rendering e chiama la funzione che assegni quando la proprietà cambia.

Prima dell'usoEffetto mi sono trovato ad aggiungere metodi del ciclo di vita innaturali e oggetti di scena OnPropertyChanged perché non avevo modo di diffondere la proprietà al di fuori del componente:

Ora con useEffect c'è un modo molto leggero per sparare sui cambi di oggetti di scena e il nostro componente reale non deve aggiungere oggetti di scena non necessari alla sua funzione.

Dichiarazione di non responsabilità: prima dell'usoEffetto c'erano modi per diffondere la prop all'interno di un contenitore usando altri componenti di ordine superiore (come ricomporre il ciclo di vita) o creare un componente del ciclo di vita come reagisce il router internamente, ma questi modi erano confusi per il team o non convenzionali.

Quali sono i vantaggi qui?

I componenti restano divertenti

Per me, la creazione di componenti è la parte più divertente e soddisfacente dello sviluppo front-end. Puoi trasformare le idee e i sogni del tuo team in esperienze reali e questa è una bella sensazione, penso che tutti noi ci relazioniamo e condividiamo.

Non ci sarà mai uno scenario in cui l'API e l'esperienza del tuo componente siano rovinate dal "mondo esterno". Il tuo componente diventa quello che hai immaginato senza ulteriori oggetti di scena - questo è il mio vantaggio preferito di questa regola d'oro.

Più opportunità per testare e riutilizzare

Quando adotti un'architettura come questa, stai essenzialmente portando in superficie un nuovo livello di dati. In questo "livello" puoi cambiare marcia quando sei più preoccupato della correttezza dei dati che entrano nel tuo componente rispetto a come funziona il componente.

Che tu ne sia consapevole o meno, questo livello esiste già nella tua app ma potrebbe essere abbinato alla logica di presentazione. Quello che ho scoperto è che quando emergo questo livello, posso fare molte ottimizzazioni del codice e riutilizzare molta logica che altrimenti riscriverei senza conoscere i punti in comune.

Penso che questo diventerà ancora più ovvio con l'aggiunta di ganci personalizzati. Gli hook personalizzati ci offrono un modo molto più semplice per estrarre la logica e sottoscrivere cambiamenti esterni, cosa che una funzione di supporto non potrebbe fare.

Massimizza la produttività della squadra

Quando si lavora in gruppo, è possibile separare lo sviluppo di contenitori e componenti. Se accetti in anticipo le API, puoi lavorare contemporaneamente su:

  1. API Web (ovvero back-end)
  2. Recupero dei dati dall'API Web (o simili) e trasformazione dei dati nelle API del componente
  3. I componenti

Ci sono delle eccezioni?

Proprio come la vera Regola d'oro, questa regola d'oro è anche una regola empirica d'oro. Esistono alcuni scenari in cui ha senso scrivere un'API componente apparentemente innaturale per ridurre la complessità di alcune trasformazioni.

Un semplice esempio sarebbe i nomi di oggetti di scena. Renderebbe le cose più complicate se gli ingegneri rinominassero le chiavi dei dati con l'argomento che è più "naturale".

È sicuramente possibile portare questa idea troppo in là dove finisci per generalizzare troppo presto e che può anche essere una trappola.

La linea di fondo

Più o meno, questa "regola d'oro" sta semplicemente rielaborando l'idea esistente dei componenti di presentazione rispetto ai componenti del contenitore sotto una nuova luce. Se valuti ciò di cui il tuo componente ha bisogno a un livello fondamentale, probabilmente finirai con parti più semplici e più leggibili.

Grazie!