Gestire lo stato in React Native

In ambito di sviluppo web, React si è affermato come uno dei framework più popolari e potenti per la creazione di interfacce utente dinamiche e reattive. Al centro della sua architettura modulare si trova il concetto di stato, fondamentale per mantenere e gestire lo stato dell’applicazione in modo efficace ed efficiente.

In questo articolo, esamineremo da vicino diverse metodologie disponibili per gestire lo stato in React: dalle soluzioni integrate come gli hook useState e useReducer, fino alle librerie di terze parti come Redux e Recoil. Ognuno di questi approcci offre vantaggi unici e si adatta a diverse esigenze di sviluppo, consentendo agli sviluppatori di creare applicazioni robuste e scalabili.

Per rendere più chiare le differenze, introdurremo un’app semplicissima che gestisce alcune variabili di stato e la implementeremo utilizzando ogni volta una metodologia diversa, illustrando nel dettaglio a cosa servono le numerose parti del codice.

L’app consiste in un campo di input, tre pulsanti e due stringhe di testo: la prima stringa riflette esattamente ciò che l’utente inserisce nel campo di input, mentre la seconda mostra la scritta “Contatore:” seguita da un valore numerico inizialmente pari a 0. Questo valore può essere incrementato o diminuito di 1 attraverso i pulsanti Sottrai e Aggiungi, inoltre entrambe le stringhe di testo possono essere colorate cliccando su un terzo pulsante Cambia, che gestisce uno stato condiviso.

1 UseState

Il modo più immediato per gestire lo stato in React Native è attraverso l’uso dello hook useState, disponibile all’interno del modulo react e fruibile da ogni componente basato su una funzione Javascript. Vediamo come è stato utilizzato all’interno del componente App:

Ciò che ritorna il metodo useState() è un array di due elementi: il primo è una variabile di stato, il secondo è la funzione che consente di modificare il valore della variabile di stato, detto anche setter. L’argomento passato a useState() è il valore di default del nostro stato:
– count è inizializzato a 0;
– input è inizializzato come stringa vuota;
– color è inizializzato come booleano false.

Quando chiamiamo il setter, possiamo passare come argomento sia il nuovo valore della variabile di stato, sia una funzione anonima. Quest’ultima prende come argomento il vecchio valore dello stato, spesso indicato con prec o prev (precedente o previous) e ritorna il nuovo valore aggiornato. Dunque la scrittura setCount(count+1) e la scrittura setCount(prev => prev+1) sono equivalenti ed entrambe accettabili.

Passare una funzione anonima come argomento al setter è particolarmente utile quando dobbiamo gestire stati più complessi, come gli array o gli oggetti. Se vogliamo cambiare il valore della proprietà prop di una variabile di stato obj attraverso il suo setter setObj:

setObj(prev => {…prev, prop: newValue})


Questa scrittura che fa uso dello spread operator equivale a mantenere invariate tutte le altre proprietà del nostro stato obj, che esse siano 2, 10 o 1000. Le proprietà che invece vogliamo modificare vanno indicate dopo lo spread operator con il classico pattern chiave:valore.

2. UseReducer

Il concetto alla base dello hook useReducer, anche esso disponibile all’interno del modulo react è quello di gestire lo stato attraverso una serie di azioni. Possiamo trovare un’ottima analogia nel metodo reduce() degli array in Javascript, dove un valore detto accumulatore viene aggiornato man mano che si itera sull’array considerato: lo stato svolge esattamente il ruolo dell’accumulatore e viene aggiornato in base alle azioni dell’utente.

La funzione responsabile dell’aggiornamento in risposta a determinate azioni prende il nome di reducer ed è il cuore pulsante di questo hook. È convenzione definire il reducer in un file a parte, esportarlo e importarlo dove necessario. Il reducer prende due parametri, uno corrispondente allo stato e l’altro all’azione.

Prima di tutto vogliamo definire quali azioni verranno processate dal reducer e in che modo. Le azioni hanno principalmente due parametri, type e payload, ma solo il primo è sicuramente non nullo e perciò viene usato per differenziare il comportamento del reducer. Usiamo la sintassi switch…case… di Javascript sul parametro type dell’azione.

Possiamo scegliere in maniera arbitraria il nome corrispondente ai type delle azioni: nel nostro caso scegliamo increment per il tasto Aggiungi, decrement per il tasto Sottrai e così via. È fondamentale chiamare la funzione return all’interno di ogni case e ritornare il nuovo stato, che in generale sarà un oggetto. Esattamente come visto prima, modifichiamo le opportune proprietà e manteniamo invariate le altre tramite l’uso dello spread operator.

Vediamo come usare il reducer nei componenti che ne fanno uso.

Si utilizza lo hook useReducer passando due argomenti: il primo è il reducer che abbiamo definito e importato, mentre il secondo è il valore iniziale del nostro oggetto di stati. La funzione useReducer() ritorna un array di due elementi, lo stato della nostra applicazione pronto a essere aggiornato dal reducer e una funzione che di solito viene chiamata dispatch.

Il compito di dispatch è di invocare azioni che verranno gestite dal reducer, passando i parametri type e payload correttamente. Nel nostro caso, le azioni di incremento del contatore, diminuzione del contatore e cambio del colore necessitano solo del parametro type per essere identificate; l’azione di inserimento dell’input invece è più complessa e il parametro payload rappresenta il testo digitato dall’utente. Questo è il motivo per cui nel codice del reducer, il case newInput è diverso dagli altri.

È importante notare come in questa seconda versione ci sia un unico oggetto di stato state: dunque dove prima usavamo color, input e count, ora abbiamo state.color, state.input e state.count, corrispondenti all’accesso in chiave.

Differenze tra useState e useReducer?

Lo hook useReducer è utile quando lo stato del componente coinvolge logiche complesse o quando ci sono molteplici azioni che possono modificare lo stato in modi diversi. Il reducer consente di separare la logica di aggiornamento dello stato in funzioni distinte, rendendo più chiare e gestibili le operazioni di aggiornamento dello stato.

Inoltre, man mano che la nostra applicazione diventa più estesa, è sempre più comune che una variabile di stato venga passata a un componente figlio, all’interno del quale il valore può essere aggiornato. L’approccio con useState ci vincola a passare non solo ogni pezzo di stato ma anche il corrispondente setter e questo può portare ad un codice confusionario.

Tanto più complesso è lo stato che vogliamo gestire, tanto più evidente saranno i vantaggi nell’utilizzo di useReducer. Se invece non facciamo uso eccessivo di stati che verranno passati da un componente padre e aggiornati da un componente figlio, useState può essere un’ottima scelta in virtù della sua semplicità e immediatezza nell’implementazione. Come sempre, dipende tutto dalle esigenze che la nostra applicazione ci pone di fronte.

3. Redux

Redux è una delle librerie più vecchie per la gestione dello stato in React/React Native, ma questo non significa che non sia rimasta al passo con i tempi. Utilizza un paradigma molto simile a quello dello hook useReducer: lo stato viene modificato in seguito ad azioni invocate dalla funzione dispatch e gestite da un reducer. Il vantaggio di Redux è che lo stato diventa un oggetto globale, dunque accessibile da ogni componente della nostra applicazione!

In passato il lato negativo di Redux era l’eccessiva complessità della sintassi e delle parti coinvolte nella definizione di uno stato globale. Da qualche anno i creatori di Redux hanno reso disponibile una libreria semplificata e altrettanto efficiente: si tratta di Redux-toolkit, diventata ormai il modo standard di implementare la logica alla base di Redux.

Le librerie da installare sono react-redux e @reduxjs/toolkit.

La prima cosa da fare è creare uno store: si tratta di una sede centralizzata per la gestione dello stato. Il comportamento che vogliamo evitare in ogni applicazione complessa è di avere due componenti che gestiscono una parte comune di stato e che non siano sincronizzati tra loro: lo store rappresenta l’unica fonte di verità e previene questa criticità.

Il modo più semplice per realizzare ciò è usare il metodo configureStore(), salvare il risultato in una variabile e passare questa variabile al parametro store di un Provider. Lo scopo del provider è di inglobare l’intera applicazione in modo che lo stato, residente nello store, sia fruibile in ogni schermata e da ogni componente. Per il momento non soffermiamoci sull’oggetto reducer.

In questo esempio il componente App è il punto di ingresso dell’applicazione e serve come contenitore del provider, mentre il componente ReduxApp è il codice vero e proprio e ha lo stesso ruolo del componente App nelle sezioni 1 e 2.

Il passo successivo consiste nella creazione degli slice: come suggerisce il nome, sono delle porzioni dello stato dell’applicazione. Mentre è tassativo utilizzare un unico store, non c’è limite al numero degli slice da utilizzare: la cosa più sensata è di raggruppare in uno slice quelle parti dello stato strettamente correlate tra loro e in caso contrario usare slice differenti.

Uno slice richiede tre parametri: il nome, lo stato iniziale e una lista di reducers. Nel nostro caso, lo slice countSlice è responsabile dello stato associato a count e dei reducers che vanno a modificare questo stato, increment e decrement. Allo stesso modo, colorInputSlice è responsabile degli stati associati a color e input, nonché dei reducers newInput e toggleColor.

In fase di export, è importante esportare sia le azioni gestite dai reducers, sia i reducer stessi. Torniamo alla configurazione dello store e riprendiamo l’oggetto reducer: possiamo notare come le chiavi di questo oggetto sono esattamente i nomi che abbiamo dato ai nostri slice, mentre i valori corrispondono ai reducer appena esportati.

Rimane solo da mettere insieme tutti i pezzi del puzzle. Nel nostro componente principale ReduxApp importiamo le azioni esportate dagli slice e anche due hook provenienti dalla libreria react-redux: useSelector() e useDispatch().

Il primo serve a destrutturare le variabili di stato: l’argomento che si passa a useSelector è una funzione anonima che prende lo stato e restituisce lo slice nel quale risiede la variabile o le variabili a cui siamo interessati. Ecco così che count viene destrutturato dallo slice identificato con counter, mentre color e input dallo slice identificato con colorInput.

Il secondo serve a istanziare una funzione dispatch con lo stesso ruolo visto nella sezione 2, a cui passiamo come argomento le azioni definite negli slice. Fatto questo, abbiamo un’app React Native perfettamente funzionante che usa Redux per la gestione dello stato.

4. Recoil

Recoil è una libreria molto più recente, sviluppata da Facebook e introdotta nel 2020. Pur non essendo popolare e diffusa quanto Redux, sta prendendo progressivamente piede nel panorama delle applicazioni React Native grazie alla sua versatilità e robustezza, qualità che le permettono di essere una seria alternativa a Redux.

Alla base di Recoil sta il concetto di atomo: con atomo si intende il più piccolo pezzo di stato possibile gestito da Recoil e ogni atomo sarà indipendente l’uno dall’altro. I nostri componenti semplicemente hanno la facoltà di leggere lo stato rappresentato da questi atomi ed eventualmente di aggiornarlo.

Quando un componente legge lo stato di un atomo, si dice che è in ascolto su quell’atomo. Un componente può essere in ascolto su un numero arbitrario di atomi: non c’è un limite ben preciso. In maniera simile, non c’è limite al numero di componenti che possono essere in ascolto su un determinato atomo. Nel momento in cui un componente aggiorna lo stato, il nuovo valore viene trasmesso correttamente a tutti i componenti che erano in ascolto sull’atomo corrispondente.

Installiamo la libreria recoil e vediamo un esempio.

Per usufruire di tutti i vantaggi messi a disposizione dalla libreria, dobbiamo racchiudere i componenti principali all’interno di RecoilRoot. Anche in questo caso, App è il punto di ingresso della nostra applicazione ma il codice vero e proprio sta nel componente RecoilApp, che dunque ha lo stesso ruolo di ReduxApp nella sezione 3.

Il passo successivo consiste nell’inizializzare gli atomi che rappresentano il nostro stato.
Creiamo un file a parte nel nostro progetto e importiamo la funzione atom dalla libreria recoil. A questa funzione passeremo sempre almeno due parametri: key è una chiave identificativa dell’atomo, mentre default è il valore iniziale dello stato associato a quell’atomo. Salviamo il risultato delle funzioni atom in alcune variabili che andranno esportate.

La libreria ci mette a disposizione tre hook: useRecoilState, useSetRecoilState e useRecoilValue. Già dal nome e dalla sintassi possiamo vedere delle analogie con il metodo useState: non a caso, uno dei vantaggi di Recoil è di essere molto alla mano per chi ha già una certa familiarità con React e React Native. Vediamo le differenze tra questi hook.

Quando vogliamo che il nostro componente possa leggere lo stato di un atomo e allo stesso tempo possa aggiornarlo, la scelta giusta è useRecoilState(). L’argomento di questa funzione sarà l’atomo corrispondente e ritorna esattamente lo stesso output di useState: una variabile di stato e il setter per modificarne il valore. Nel nostro esempio, passiamo come argomento a questo hook countState e inputState per controllare rispettivamente count e input.

Se vogliamo che il nostro componente possa aggiornare il valore di un atomo ma non è necessario che ne legga lo stato, utilizzeremo useSetRecoilState(). Questa funzione ritorna solamente un setter e non la variabile di stato corrispondente. Nel nostro esempio, è il caso dell’atomo colorState all’interno del componente RecoilApp: il colore viene modificato dal tasto Cambia, ma non viene consumato da RecoilApp, almeno non direttamente.

Lo stato associato al colore viene consumato all’interno del componente ColorText.

Se un componente legge il valore di un atomo ma non è necessario che sia in grado di aggiornarlo, siamo nella casistica di useRecoilValue(). Questa funzione ritorna solo la variabile di stato e non ritorna il setter corrispondente. Poiché il colore viene aggiornato da un tasto esterno a ColorText, non ci sono problemi; al contrario, utilizzare gli hook opportuni nei diversi componenti rende comprensibile fin da subito chi si occupa della lettura di uno stato e chi si occupa dell’aggiornamento.

Se dovessimo azzardare un paragone tra Redux e Recoil, la prima offre un maggiore range di funzionalità, richiede una struttura leggermente più articolata per la creazione di uno stato globale ed è particolarmente indicata per la gestione di stati complessi. Recoil d’altro canto è una libreria improntata alla semplicità e il suo modo di gestire lo stato è sicuramente più immediato per chi ha familiarità con React, rendendola un’alternativa di assoluto valore.

Vuoi maggiori informazioni?

Cerchi sviluppatori React Native a supporto delle tue attività professionali? 

Compila i campi qui sotto, ti ricontatteremo quanto prima.