Gestione dello stato in React semplificata: separa lo stato server da quello client, segui poche regole e individua presto i segnali di aumento della complessità.

Lo stato è qualsiasi dato che può cambiare mentre la tua app è in esecuzione. Include quello che vedi (una modal aperta), quello che stai modificando (una bozza di form) e i dati che recuperi (una lista di progetti). Il problema è che tutto questo viene chiamato "stato", anche se si comportano in modo molto diverso.
La maggior parte delle app disordinate si rompe nello stesso modo: troppi tipi di stato vengono mescolati nello stesso posto. Un componente finisce per contenere dati server, flag UI, bozze di form e valori derivati, e poi prova a tenerli allineati con effetti. Prima o poi non sai più rispondere a domande semplici come "da dove viene questo valore?" o "chi lo aggiorna?" senza cercare in più file.
Le app React generate si ritrovano più rapidamente in questa situazione perché è facile accettare la prima versione funzionante. Aggiungi una nuova schermata, copi un pattern, sistemi un bug con un altro useEffect e ora hai due fonti di verità. Se il generatore o il team cambia direzione a metà strada (stato locale qui, store globale là), il codebase colleziona pattern invece di costruire su uno solo.
L'obiettivo è noioso: meno tipi di stato e meno posti da controllare. Quando c'è una casa ovvia per i dati server e una casa ovvia per lo stato solo UI, i bug diventano più piccoli e le modifiche smettono di sembrare rischiose.
"Mantienilo noioso" significa seguire poche regole:
Un esempio concreto: se una lista di utenti viene dal backend, trattala come server state e falla fetchare dove viene usata. Se selectedUserId esiste solo per pilotare un pannello dettagli, tienilo come stato UI vicino a quel pannello. Mescolare i due è l'inizio della complessità.
La maggior parte dei problemi di stato in React inizia con una confusione: trattare i dati server come stato UI. Separa prima i due e la gestione dello stato resta calma, anche quando l'app cresce.
Lo stato server appartiene al backend: utenti, ordini, task, permessi, prezzi, feature flag. Può cambiare senza che la tua app faccia nulla (un'altra tab lo aggiorna, un admin lo modifica, un job lo aggiorna, i dati scadono). Poiché è condiviso e modificabile, serve fetching, caching, refetching e gestione degli errori.
Lo stato client è quello che interessa solo alla tua UI in questo momento: quale modal è aperta, quale tab è selezionata, un toggle di filtro, l'ordine di ordinamento, una sidebar compressa, una bozza di ricerca. Se chiudi la scheda, può andare bene perderlo.
Un test rapido è: "Potrei ricaricare la pagina e ricostruire questo dal server?"
Esiste anche lo stato derivato, che ti evita di creare stato extra. È un valore che puoi calcolare da altri valori, quindi non lo memorizzi. Liste filtrate, totali, isFormValid e "mostra stato vuoto" di solito appartengono qui.
Esempio: recuperi una lista di progetti (server state). Il filtro selezionato e la flag del dialog "Nuovo progetto" sono client state. La lista visibile dopo il filtro è stato derivato. Se memorizzi la lista visibile separatamente, si sfaserà e ti ritroverai a inseguire bug del tipo "perché è obsoleta?".
Questa separazione aiuta quando uno strumento come Koder.ai genera schermate velocemente: tieni i dati backend in uno strato di fetching, tieni le scelte UI vicino ai componenti e evita di salvare valori calcolati.
Lo stato diventa doloroso quando un pezzo di dati ha due proprietari. Il modo più veloce per mantenere le cose semplici è decidere chi possiede cosa e rispettarlo.
Esempio: fetchi una lista di utenti e mostri i dettagli quando uno è selezionato. Un errore comune è salvare l'intero oggetto utente selezionato nello stato. Salva selectedUserId invece. Tieni la lista nella cache server. La vista dettagli cerca l'utente per ID, così i refetch aggiornano la UI senza codice di sincronizzazione extra.
Nelle app React generate, è facile accettare stato "utile" generato che duplica i dati server. Quando vedi codice che fa fetch -> setState -> edit -> refetch, fermati. Spesso è il segno che stai costruendo un secondo database nel browser.
Lo stato server è tutto ciò che vive nel backend: liste, pagine di dettaglio, risultati di ricerca, permessi, conteggi. L'approccio noioso è scegliere uno strumento per questo e restarci. Per molte app React, TanStack Query è sufficiente.
L'obiettivo è semplice: i componenti chiedono i dati, mostrano loading e error, e non si preoccupano di quante chiamate fetch avvengono sotto il cofano. Questo è importante nelle app generate perché piccole incongruenze si moltiplicano velocemente con nuove schermate.
Tratta le chiavi di query come un sistema di naming, non come un ripensamento. Mantienile coerenti: chiavi array stabili, includi solo gli input che cambiano il risultato (filtri, pagina, ordinamento) e preferisci poche forme prevedibili rispetto a molte eccezioni. Molti team mettono la costruzione delle key in helper piccoli in modo che ogni schermata usi le stesse regole.
Per le scritture, usa mutations con gestione esplicita del successo. Una mutation dovrebbe rispondere a due domande: cosa è cambiato e cosa deve fare la UI dopo?
Esempio: crei un nuovo task. Al successo, o invalidi la query della lista tasks (così si ricarica) oppure fai un aggiornamento mirato della cache (aggiungi il nuovo task alla lista cached). Scegli un approccio per funzionalità e mantienilo coerente.
Se ti senti tentato di aggiungere chiamate di refetch in più posti "per sicurezza", scegli una mossa noiosa invece:
Lo stato client è ciò che il browser possiede: una flag di sidebar aperta, una riga selezionata, testo di filtro, una bozza prima di salvare. Tienilo vicino al punto di utilizzo e generalmente resta gestibile.
Parti in piccolo: useState nel componente più vicino. Quando generi schermate (per esempio con Koder.ai), è tentante mettere tutto in uno store globale "per sicurezza". È così che finisci con uno store che nessuno capisce.
Sposta lo stato verso l'alto solo quando sai nominare il problema di condivisione.
Esempio: una tabella con un pannello dettagli può tenere selectedRowId nel componente tabella. Se una toolbar in un'altra parte della pagina ne ha bisogno, rialzalo al componente pagina. Se una route separata (come bulk edit) ne ha bisogno, allora uno store piccolo può avere senso.
Se usi uno store (Zustand o simili), tienilo focalizzato su un lavoro. Salva il "cosa" (ID selezionati, filtri), non i "risultati" (liste ordinate) che puoi derivare.
Quando uno store inizia a crescere, chiediti: è ancora una sola feature? Se la risposta sincera è "più o meno", dividilo ora, prima che la prossima feature lo trasformi in una palla di stato che fai fatica a toccare.
I bug dei form spesso vengono dalla mescolanza di tre cose: quello che l'utente sta digitando, quello che il server ha salvato e quello che la UI mostra.
Per una gestione noiosa dello stato, tratta il form come client state finché non invii. I dati server sono l'ultima versione salvata. Il form è una bozza. Non modificare l'oggetto server in place. Copia i valori nello stato bozza, lascia che l'utente li cambi liberamente, poi invia e refetcha (o aggiorna la cache) al successo.
Decidi presto cosa deve persistere quando l'utente naviga via. Quella singola scelta previene molte sorprese. Per esempio, una modalità inline edit e dropdown aperti dovrebbero di solito resettarsi, mentre una bozza di wizard lunga o un messaggio non inviato potrebbe persistere. Persisti attraverso il reload solo quando gli utenti se lo aspettano chiaramente (come in un checkout).
Tieni le regole di validazione in un solo posto. Se disperdi regole tra input, handler di submit e helper, finirai con errori non corrispondenti. Preferisci uno schema (o una funzione validate()), e lascia che la UI decida quando mostrare gli errori (on change, on blur o on submit).
Esempio: generi una schermata Edit Profile in Koder.ai. Carica il profilo salvato come server state. Crea stato bozza per i campi del form. Mostra "modifiche non salvate" confrontando bozza vs salvato. Se l'utente cancella, scarta la bozza e mostra la versione server. Se salva, invia la bozza, poi sostituisci la versione salvata con la risposta del server.
Man mano che un'app generata cresce, è comune ritrovarsi con gli stessi dati in tre posti: stato del componente, uno store globale e una cache. La correzione di solito non è una nuova libreria. È scegliere una casa per ogni pezzo di stato.
Un flusso di pulizia che funziona nella maggior parte delle app:
filteredUsers se puoi calcolarlo da users + filter. Preferisci selectedUserId rispetto a un selectedUser duplicato.Esempio: un'app CRUD generata da Koder.ai spesso parte con un useEffect che fetcha più una copia globale dello stesso elenco. Dopo aver centralizzato lo stato server, la lista proviene da una sola query e "refresh" diventa invalidation invece di syncing manuale.
Per il naming, mantienilo coerente e noioso:
users.list, users.detail(id)ui.isCreateModalOpen, filters.userSearchopenCreateModal(), setUserSearch(value)users.create, users.update, users.deleteL'obiettivo è una sola fonte di verità per ogni cosa, con confini chiari tra stato server e stato client.
I problemi di stato iniziano piccoli, poi un giorno cambi un campo e tre parti della UI non sono d'accordo sul valore "reale".
Il segnale più chiaro è il dato duplicato: lo stesso utente o carrello vive in un componente, uno store globale e una cache di richiesta. Ogni copia si aggiorna a tempi diversi e aggiungi altro codice solo per tenerle uguali.
Un altro segnale è il codice di sincronizzazione: effetti che spingono stato avanti e indietro. Pattern come "quando la query cambia, aggiorna lo store" e "quando lo store cambia, refetch" possono funzionare finché un caso limite non provoca valori obsoleti o loop.
Alcuni segnali di pericolo rapidi:
needsRefresh, didInit, isSaving che nessuno rimuove.Esempio: generi una dashboard in Koder.ai e aggiungi una modal Edit Profile. Se i dati del profilo sono nella query cache, copiati in uno store globale e duplicati nello stato locale del form, ora hai tre fonti di verità. Appena aggiungi refetch in background o aggiornamenti ottimistici, compaiono incongruenze.
Quando vedi questi segnali, la mossa noiosa è scegliere un proprietario singolo per ogni pezzo di dato e cancellare i duplicati.
Salvare cose "nel caso servisse" è uno dei modi più rapidi per peggiorare lo stato, specialmente in app generate.
Copiare risposte API nello store globale è una trappola comune. Se i dati vengono dal server (liste, dettagli, profilo), non copiarli nello store client per default. Scegli una casa per i dati server (di solito la cache delle query). Usa lo store client per valori UI che il server non conosce.
Salvare valori derivati è un'altra trappola. Conteggi, liste filtrate, totali, canSubmit e isEmpty dovrebbero di solito essere calcolati dagli input. Se le prestazioni diventano un problema reale, memoizza dopo aver misurato, ma non iniziare salvando il risultato.
Un mega-store per tutto (auth, modali, toasts, filtri, bozze, flag onboarding) diventa una discarica. Dividi per confini di feature. Se lo stato è usato solo da una schermata, tienilo locale.
Context è ottimo per valori stabili (theme, user id corrente, locale). Per valori che cambiano spesso, può causare re-render estesi. Usa Context per wiring, e stato del componente (o un piccolo store) per valori UI che cambiano frequentemente.
Infine, evita naming incoerente. Chiavi di query e campi di store quasi identici creano duplicazioni sottili. Scegli uno standard semplice e seguilo.
Quando ti viene la voglia di aggiungere "solo un'altra" variabile di stato, fai un controllo rapido di proprietà.
Primo: puoi indicare un posto unico dove avviene fetching e caching server (uno strumento di query, un set di chiavi)? Se gli stessi dati vengono fetchati in più componenti e anche copiati in uno store, stai già pagando interessi.
Secondo: questo valore serve solo dentro una schermata (es. "pannello filtri aperto")? Se sì, non dovrebbe essere globale.
Terzo: puoi salvare un ID invece di duplicare un oggetto? Salva selectedUserId e leggi l'utente dalla cache o dalla lista.
Quarto: è derivato? Se lo puoi calcolare da stato esistente, non salvarlo.
Infine, fai il test di tracciamento di un minuto. Se un collega non riesce a rispondere "da dove viene questo valore?" (prop, stato locale, cache server, URL, store) in meno di un minuto, sistema la proprietà prima di aggiungere altro stato.
Immagina una admin app generata (per esempio da un prompt in Koder.ai) con tre schermate: lista clienti, pagina dettaglio cliente e form di modifica.
Lo stato resta calmo quando ha case ovvie:
La lista e le pagine dettaglio leggono lo stato server dalla cache delle query. Quando salvi, non riscrivi i clienti nello store globale: mandi la mutation e lasci che la cache si aggiorni o venga invalidata.
Per la schermata di modifica, tieni la bozza del form locale. Inizializzala dal cliente fetchato, ma trattala come separata mentre l'utente digita. Così la vista dettaglio può aggiornarsi senza sovrascrivere modifiche a metà.
L'UI ottimistica è un punto dove i team spesso duplicano tutto. Di solito non è necessario.
Quando l'utente preme Salva, aggiorna solo il record cached e l'item corrispondente nella lista, poi fai rollback se la richiesta fallisce. Tieni la bozza nel form finché il salvataggio non riesce. Se fallisce, mostra un errore e mantieni la bozza così l'utente può riprovare.
Supponiamo che aggiungi bulk edit che ha bisogno delle righe selezionate. Prima di creare un nuovo store, chiediti: questo stato deve sopravvivere a navigazione e refresh?
Le schermate generate possono moltiplicarsi velocemente, ed è ottimo finché ogni nuova schermata non porta le sue decisioni sullo stato.
Scrivete una breve nota di team nel repo: cosa è server state, cosa è client state e quale strumento possiede ciascuno. Mantienila sufficientemente breve perché la gente la segua.
Aggiungi un'abitudine nelle PR: etichettare ogni nuovo pezzo di stato come server o client. Se è server, chiedi "dove si carica, come è cached e cosa lo invalida?" Se è client, chiedi "chi lo possiede e quando si resetta?"
Se usi Koder.ai (koder.ai), Planning Mode può aiutare a decidere i confini di proprietà prima di generare nuove schermate. Snapshot e rollback ti danno un modo sicuro per sperimentare quando una modifica di stato va male.
Scegli una feature (come modifica profilo), applica le regole end-to-end e lascia che sia l'esempio che tutti copiano.
Inizia etichettando ogni pezzo di stato come server, client (UI) o derivato.
isValid).Una volta etichettati, assicurati che ogni elemento abbia un proprietario ovvio (cache delle query, stato locale del componente, URL o un piccolo store).
Usa questo test rapido: “Posso ricaricare la pagina e ricostruirlo dal server?”
Esempio: una lista di progetti è server state; l'ID della riga selezionata è client state.
Perché crea due fonti di verità.
Se fetchi users e poi li copi in useState o in uno store globale, ora devi mantenerli sincronizzati durante:
Regola predefinita: e crea stato locale solo per preoccupazioni UI o bozze.
Salva valori derivati solo quando davvero non puoi calcolarli in modo economico.
Di solito calcoli da input esistenti:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingSe le prestazioni diventano un problema reale (misurato), preferisci o strutture dati migliori prima di introdurre altro stato memorizzato che può diventare obsoleto.
Default: usa uno strumento per lo stato server (spesso TanStack Query) così i componenti possono semplicemente “chiedere i dati” e gestire loading/error.
Basi pratiche:
Lascialo locale finché non sai nominare un reale bisogno di condivisione.
Regola di promozione:
Questo evita che lo store globale diventi una discarica di flag UI casuali.
Salva ID e flag piccoli, non oggetti server completi.
Esempio:
selectedUserIdselectedUser (oggetto copiato)Poi rendi i dettagli cercando l'utente nella lista cached o nella query detail. Così i refetch in background e gli aggiornamenti funzionano senza codice di sincronizzazione extra.
Tratta il form come una bozza (stato client) finché non invii.
Pattern pratico:
Così eviti di modificare i dati server “in place” e conflitti con i refetch.
Segnali di allarme comuni:
needsRefresh, didInit, isSaving.Conservare cose “nel caso servisse” è una delle vie più rapide per rendere lo stato ingestibile, soprattutto in app generate.
Copiare risposte API in uno store globale è una trappola comune. Se i dati vengono dal server (liste, dettagli, profilo utente), non copiarli nello store client per default. Scegli una casa per i dati server (di solito la cache delle query). Usa lo store client per valori UI che il server non conosce.
Un mega-store che contiene tutto diventa una discarica. Se uno stato è usato solo da una schermata, tienilo locale.
Usa Context per valori stabili (theme, user id corrente, locale). Per valori che cambiano frequentemente, Context può causare troppi rerender: usa stato del componente o un piccolo store.
Quando senti la voglia di aggiungere “solo un’altra” variabile di stato, fai un rapido controllo di proprietà.
Infine, fai il test di tracciamento di un minuto: se un collega non riesce a rispondere “da dove viene questo valore?” (prop, stato locale, cache server, URL, store) in meno di un minuto, sistema la proprietà prima di aggiungere altro stato.
Immagina una admin app generata con tre schermate: lista clienti, dettaglio cliente e form di modifica.
Lo stato resta calmo quando ha case ovvie:
Lista e dettagli leggono dallo stesso cache di query. Al salvataggio non riscrivi i clienti nello store globale: invii la mutation e lasci che la cache si aggiorni o venga invalidata.
Per l’edit, tieni la bozza localmente. Inizializzala dal cliente fetchato, ma trattala come separata mentre l’utente digita. Così la vista dettaglio può aggiornarsi senza sovrascrivere modifiche in corso.
Quando premi Salva con ottimismo, aggiorna solo il record cached e l'item corrispondente nella lista, poi rollback se la richiesta fallisce. Tieni la bozza nel form finché il salvataggio non riesce. Se fallisce, mostra un errore e lascia la bozza così l'utente può riprovare.
Se aggiungi, per esempio, un bulk edit che ha bisogno delle stesse righe selezionate, chiediti prima: questo stato deve sopravvivere alla navigazione o al refresh?
Le schermate generate possono moltiplicarsi in fretta. Scrivete una nota breve nel repo: cosa conta come server state, cosa come client state e quale strumento possiede ciascuno. Mantienila abbastanza corta perché la gente la segua.
Aggiungi una piccola abitudine in PR: etichettate ogni nuovo pezzo di stato come server o client. Se è server, chiedete “dove si carica, come è cached e cosa lo invalida?” Se è client, chiedete “chi lo possiede e quando si resetta?”
Se usi Koder.ai, Planning Mode può aiutare a concordare i confini di proprietà prima di generare nuove schermate. Snapshot e rollback danno un modo sicuro per sperimentare quando una modifica di stato va male.
Scegli una funzionalità (es. modifica profilo), applica le regole end-to-end e lascia che sia l’esempio che tutti copiano.
useMemoEvita di spargere refetch() ovunque “per sicurezza”.
La soluzione spesso non è una nuova libreria: è cancellare i duplicati e scegliere un proprietario per ogni valore.