Apri quasi ogni codebase React e noterai lo stesso riflesso. Uno sviluppatore ha bisogno di tracciare un valore, quindi si rivolge a useState. Serve un contatore? useState. Un valore di input temporaneo? useState. Un booleano per attivare una modale? useState. In breve tempo, un singolo componente ospita una dozzina di hook separati, ognuno dei quali gestisce un frammento di dati che potrebbe o meno aver bisogno di persistere tra i render. Il risultato è un codice rumoroso, re-render extra e uno stato disperso come spiccioli all'interno di un componente.
L'abitudine è comprensibile. useState è il primo hook che la maggior parte di noi impara, e funziona. Ma funzionare non significa essere adatti. Trattare ogni pezzo di dati come stato reattivo crea problemi che si manifestano solo quando il componente cresce.
Il fatto che cambi non significa che abbia bisogno di uno stato
Non ogni variabile che cambia nel tempo deve appartenere a useState. Alcuni valori sono semplicemente il risultato di qualcos'altro che hai già a disposizione. Se memorizzi il nome completo di un utente nello stato solo perché concateni firstName e lastName, avrai ora due fonti di verità. Quando firstName si aggiorna perché il componente padre effettua un re-render, il tuo stato fullName rimarrà obsoleto finché non eseguirai un altro effect per sincronizzarlo. Non hai bisogno di un effect di sincronizzazione. Hai bisogno di un valore derivato.
const fullName = `${firstName} ${lastName}`;
Calcolalo durante il render. Se la derivazione è costosa, usa la memoizzazione. Ma non assegnargli un proprio hook useState, a meno che l'utente non possa modificare quel nome completo indipendentemente dalle sue parti.
La stessa regola si applica alle liste filtrate. Se mantieni sia allItems che filteredItems nello stato, hai raddoppiato la superficie di manutenzione. Filtra durante il render. Mantieni l'array sorgente e il testo del filtro nello stato, quindi deriva la lista visibile. Questo garantisce che la lista filtrata non sia mai fuori sincrono con la sorgente.
Alcuni valori non dovrebbero mai innescare re-render
useState esiste specificamente per comunicare a React che qualcosa è cambiato e che il DOM potrebbe aver bisogno di un aggiornamento. Se un valore cambia ma nessuna parte della UI si cura di quel cambiamento, useRef è lo strumento migliore.
I timer e gli intervalli sono l'esempio classico. Memorizzare gli ID di setInterval nello stato causa un re-render ogni volta che si avvia o si ferma un timer, anche se l'utente non può vedere l'ID dell'intervallo. Un ref mantiene quel valore senza notificare React. La stessa logica si applica al tracciamento delle prop precedenti, alla misurazione dei nodi DOM prima del paint, o alla memorizzazione dell'ultimo callback per un hook personalizzato. Chiediti: questo valore deve apparire sullo schermo? Se la risposta è no, probabilmente non ha bisogno di useState.
Anche i nodi DOM appartengono ai ref. Sebbene sia possibile memorizzare un elemento DOM nello stato, farlo innesca un re-render dopo l'esecuzione della callback del ref. Nella maggior parte dei casi, hai bisogno del nodo solo per un metodo imperativo o una misurazione, non per renderlo in modo diverso.
La trappola dei booleani
Le preoccupazioni relative alla UI tendono a espandersi quando ogni flag riceve il proprio hook. Vedrai componenti con isLoading, isError e isSuccess definiti come tre booleani separati. Il problema è che questi tre stati non sono indipendenti. Se isLoading e isSuccess sono entrambi true, la tua UI si trova in una condizione impossibile, eppure TypeScript e React ti permetteranno comunque di renderizzarla.
Raggruppare lo stato correlato previene queste combinazioni non valide. Invece di tre booleani, traccia una singola stringa di stato: 'idle', 'loading', 'success' o 'error'. Solo uno può essere attivo alla volta, il che elimina gli stati impossibili a livello di tipo. Se i dati sono più complessi, un oggetto con una discriminated union pulisce ulteriormente la situazione. Quando ti ritrovi ad aggiornare diverse chiamate useState all'interno dello stesso event handler, quello è un segnale che quei valori dovrebbero stare insieme.
Rivolgiti a useReducer, non a un altro useState
C'è un punto in cui gli aggiornamenti dello stato diventano un gioco di "colpisci il topo". Chiami setA, poi setB, poi condizionalmente setC, tutto all'interno di una singola funzione. Il prossimo sviluppatore che leggerà quel codice dovrà seguire l'intera sequenza per capire cosa faccia effettivamente il componente.
useReducer brilla in questo contesto. Non sostituisce useState perché è più avanzato; sostituisce useState perché la logica lo richiede. Un reducer centralizza il modo in cui lo stato cambia. Invece di spargere comandi imperativi tra gli event handler, invii un'intenzione: dispatch({ type: 'submitted' }). Il reducer decide quale sarà il prossimo stato. Questo rende i test banali, perché la logica dello stato è una funzione pura. Rende anche più facile il debugging, perché ogni cambiamento lascia un'azione tracciabile.
Non hai bisogno di Redux per giustificare l'uso di un reducer. Se hai tre o più variabili di stato che si aggiornano insieme, o se il tuo stato successivo dipende fortemente da quello precedente, un reducer semplifica drasticamente il componente.
Dove risiede effettivamente lo stato
A volte il problema non è come memorizzi lo stato, ma dove. Un errore comune è elevare lo stato a un componente genitore solo perché potrebbe servire altrove. Se solo un componente foglia utilizza una parte dello stato, mantienila lì. Questa è la colocation, e riduce il raggio d'impatto delle modifiche. Non far eseguire il re-render al genitore perché un figlio ha aperto
