- Lo stato in React deve essere trattato come immutabile, con aggiornamenti eseguiti tramite metodi setter anziché tramite mutazione diretta, soprattutto per oggetti e array.
- Gli aggiornamenti di stato sono asincroni e possono essere raggruppati in batch, quindi l'utilizzo di aggiornatori funzionali evita problemi di stato obsoleto in timer, closure e interazioni rapide.
- I componenti funzionali con Hook (useState, useRef e simili) rappresentano lo standard moderno, mentre strumenti come React.memo e Immer contribuiscono a migliorare le prestazioni e la gestione dei dati annidati.
- Una netta separazione tra props e state, unita a un modello di flusso dati top-down, garantisce la prevedibilità del comportamento dei componenti man mano che le applicazioni si espandono.
Lo stato è uno di quei concetti di React che a prima vista sembra semplice, ma che diventa rapidamente complesso man mano che l'applicazione cresce. Si inizia con un piccolo contatore, poi all'improvviso ci si ritrova a gestire più campi modulo, aggiornamenti asincroni, oggetti annidati e problemi di prestazioni quando tutto viene renderizzato contemporaneamente. Comprendere a fondo lo stato è ciò che distingue chi "usa React" da chi è in grado di scalare e debuggare applicazioni React reali.
In questa guida esamineremo lo stato attuale di React (gioco di parole voluto), dai componenti di classe e metodi del ciclo di vita fino ai moderni Hook e agli aggiornamenti immutabili. Approfondiremo anche argomenti sottili ma cruciali come gli aggiornamenti asincroni, le closure obsolete, quando usare `useRef` invece di `useState` e come mantenere prevedibile l'interfaccia utente. L'obiettivo è fornirti un modello mentale chiaro in modo che i tuoi componenti si comportino esattamente come ti aspetti.
Dagli oggetti di scena allo stato: cosa si colloca esattamente dove?
Alla base di ogni componente React ci sono due fonti di dati principali: props e state. Puntelli vengono passati dal componente padre e rimangono fissi per tutta la durata di quel rendering, mentre stato è di proprietà e controllato dal componente stesso ed è destinato a dati che cambiano nel tempo.
Una buona regola generale è: se i dati vengono configurati dall'esterno e non cambiano all'interno del componente, si tratta di una prop; se invece il componente deve monitorarli e aggiornarli, si tratta di uno state. Immaginate un componente di testo lampeggiante: il testo effettivo viene fornito una sola volta (una prop), ma il suo stato, se visualizzato o nascosto, cambia continuamente (stato). Questa distinzione è ciò che permette a React di mantenere il flusso di dati prevedibile e unidirezionale.
React incoraggia un flusso di dati dall'alto verso il basso (unidirezionale) in cui lo stato risiede nell'antenato comune più vicino che deve controllarlo. Un componente padre può gestire lo stato e passare i valori come props ai componenti figli, i quali possono renderizzarli o trasformarli, ma non hanno bisogno di sapere se quei valori provenissero originariamente dallo stato, da altre props o fossero codificati in modo statico.
Ecco perché si sente spesso dire che lo Stato è "locale" o "incapsulato". Solo il componente che possiede uno stato può modificarlo, e qualsiasi elemento dell'interfaccia utente derivato da tale stato si propaga verso il basso tramite le props. È possibile combinare liberamente componenti con stato e senza stato (pure), e la presenza o meno di stato è considerata un dettaglio di implementazione che può cambiare nel tempo.
Componenti di classe: stato e ciclo di vita alla vecchia maniera
Prima dell'introduzione degli Hook, l'unico modo per utilizzare lo stato e i metodi del ciclo di vita in React era tramite i componenti di classe ES6. Sebbene la maggior parte delle app moderne si basi su componenti funzionali, in molti codebase si trovano ancora (e a volte si gestiscono) componenti di classe, quindi vale la pena capire come funzionano.
Per convertire un componente funzionale come un semplice Clock Per entrare in una classe, si seguono alcuni passaggi meccanici. Si crea una classe che estende React.Component, aggiungere un render() metodo, sposta il corpo della funzione in render, sostituire props con this.propse cancella la funzione originale. Finché React continua a renderizzare <Clock /> nello stesso nodo DOM, riutilizza una singola istanza di quella classe.
Aggiungere uno stato locale a una classe significa definire un costruttore e assegnare un valore iniziale this.state oggetto. Ad esempio potresti spostare un date valore dalle props nello stato aggiungendo un costruttore che chiama super(props) e set this.state = { date: new Date() }, quindi sostituendo qualsiasi utilizzo di this.props.date in render() con this.state.date. Ricorda che nei componenti di classe dovresti assegnare direttamente solo a this.state all'interno del costruttore.
I metodi del ciclo di vita sono metodi di classe speciali che React chiama in punti specifici del ciclo di vita di un componente. Quando un componente viene inserito per la prima volta nel DOM (montato), React chiama componentDidMount(). Quando viene rimosso (smontato), React chiama componentWillUnmount(). Nell'esempio classico dell'orologio a tempo, si imposta un timer in componentDidMount e cancellarlo in componentWillUnmount, memorizzando l'ID del timer su this (per esempio this.timerId), e chiamando this.setState() ogni secondo per aggiornare l'ora.
Il ciclo di vita tipico di quell'orologio è il seguente: React chiama il costruttore per inizializzare lo stato, quindi render() per produrre il DOM, quindi componentDidMount() dove si avvia il timer. Ogni volta che il timer si attiva, si chiama setState(), che mette in coda un aggiornamento e attiva render() con il nuovo stato. Una volta rimosso il componente, componentWillUnmount() azzera il timer per evitare perdite di risorse.
Gestire correttamente lo stato nelle classi significa anche rispettare tre regole importanti riguardo setState. Non devi mutare this.state In primo luogo, è necessario ricordare che gli aggiornamenti possono essere asincroni e in batch, e bisogna comprendere che gli aggiornamenti vengono uniti superficialmente (vengono unite solo le chiavi di stato di livello superiore, non gli oggetti annidati in profondità).
Utilizzo corretto dello stato: mutazioni, aggiornamenti asincroni e flusso di dati
Una delle maggiori fonti di confusione per i principianti è che setState (e l'equivalente Hook) non aggiorna lo stato immediatamente e non dovresti mai modificare gli oggetti di stato sul posto. React spesso raggruppa più aggiornamenti insieme per migliorare le prestazioni, quindi entrambi this.state Le variabili di stato e di classe negli Hook potrebbero non riflettere lo stato finale subito dopo aver pianificato un aggiornamento.
Modificare direttamente lo stato, come fare this.state.count++ oppure la modifica delle proprietà di un oggetto di stato, ignora il rilevamento delle modifiche di React e può causare il blocco dei componenti sui vecchi valori. React prevede che ogni oggetto nello stato venga trattato come di sola lettura. Invece di modificare gli oggetti esistenti, si crea un nuovo oggetto o array con le modifiche desiderate e lo si passa al gestore dell'aggiornamento dello stato.
Poiché gli aggiornamenti di stato possono essere asincroni, è necessario prestare attenzione quando si calcola lo stato successivo a partire da quello precedente. In classe, qualcosa del genere this.setState({ count: this.state.count + 1 }) può risultare errato se vengono eseguiti più aggiornamenti in batch. La soluzione consiste nell'utilizzare la forma funzionale: this.setState((prevState, props) => ({ count: prevState.count + 1 }))Questo garantisce che tu stia lavorando con l'istantanea dello stato più recente.
Lo stesso schema si applica anche agli Hook: è possibile richiamare l'aggiornatore passando una funzione anziché un valore. Per esempio, setCount(prev => prev + 1) è il modo più sicuro per incrementare un contatore se il nuovo valore dipende da quello precedente o se gli aggiornamenti potrebbero avvenire all'interno di timer o gestori di eventi che vengono eseguiti in seguito.
Sebbene lo stato sia "locale", l'effetto di una modifica di stato si propaga sempre lungo l'albero dei componenti. Un re-rendering del genitore, attivato da un aggiornamento di stato, comporterà per impostazione predefinita anche il re-rendering di tutti i suoi figli. Questo flusso di dati dall'alto verso il basso è fondamentale per il modello mentale di React: un'unica fonte di verità in alto, con l'interfaccia utente derivata da essa in basso.
React moderno: Hook e componenti funzionali
A partire da React 16.8, gli Hook sono diventati il metodo standard per gestire lo stato e gli effetti collaterali nei componenti funzionali. Ti permettono di utilizzare le stesse funzionalità che avevano i componenti di classe (e altro) senza scrivere classi o occuparti di this e metodi del ciclo di vita in modo esplicito, apoyándose en el stato stabile di JavaScript moderno.
I componenti funzionali sono ora lo stile predefinito nei codebase di React. Invece di scrivere class Example extends React.Component, si definisce una semplice funzione come function Example() { return <div />; }. Quando hai bisogno di stato, effetti collaterali o riferimenti, ti "agganci" a React tramite funzioni come useState, useEffect e useRefGli hook non possono essere utilizzati all'interno delle classi e devono rispettare le regole degli hook (devono essere sempre richiamati al livello superiore del componente, mai all'interno di cicli o condizioni).
Migliori useState L'hook è il modo più semplice per aggiungere uno stato locale a un componente funzionale. Prende il valore iniziale come argomento e restituisce una coppia: il valore dello stato corrente e un metodo setter. Grazie alla destrutturazione degli array, di solito si scrive qualcosa del genere const = useState(0)React preserva questo stato tra un rendering e l'altro, il che significa che la funzione può essere chiamata più volte ma il valore dello stato viene memorizzato.
A differenza dello stato della classe, il valore che mantieni in useState Non deve necessariamente essere un oggetto. È possibile memorizzare numeri, stringhe, valori booleani, array o oggetti, a seconda delle esigenze dei dati. Se sono necessari più valori indipendenti, è possibile chiamare useState diverse volte (ad esempio, age, fruit, todosIn alternativa, è possibile memorizzare un singolo oggetto e gestire al suo interno più proprietà, ma è necessario rispettare le regole di immutabilità durante l'aggiornamento.
Quando chiami la funzione setter restituita da useState, non stai modificando il valore in modo sincrono; stai mettendo in coda un aggiornamento proprio come con setState in classe. Al rendering successivo, React assegna al componente il nuovo valore dello stato. Ecco perché leggere lo stato immediatamente dopo aver chiamato il setter all'interno della stessa funzione sincrona restituirà ancora il vecchio valore.
Gestione di oggetti e dati annidati nello stato
React permette di inserire qualsiasi valore JavaScript nello stato, inclusi oggetti e array, ma è necessario trattarli come istantanee immutabili. I valori primitivi come numeri e stringhe non possono essere modificati in alcun modo, mentre oggetti e array tecnicamente sì; tuttavia, modificarli viola i presupposti di React e può portare a bug difficili da individuare, per cui i componenti non si aggiornano.
Consideriamo un oggetto di stato come { x: 0, y: 0 } che rappresenta la posizione di un puntatore. Se scrivi position.x = event.clientX direttamente, hai modificato l'oggetto esistente. React non ha idea che il valore sia cambiato perché non hai mai chiamato il setter, quindi non verrà eseguito nuovamente il rendering e la tua interfaccia utente rimarrà bloccata. L'approccio corretto è setPosition({ x: event.clientX, y: event.clientY }), che crea un oggetto completamente nuovo e dice a React di eseguirne il rendering.
La modifica locale di oggetti appena creati è perfettamente accettabile. Ad esempio, è possibile costruire un nuovo oggetto passo dopo passo: const next = { ...prev }; next.city = 'Paris'; fintanto che next non era già in quello stato. La mutazione diventa un problema solo quando si modifica un oggetto che è già in uso in un'istantanea di stato precedente, perché altre parti dell'app potrebbero ancora dipendere da quel vecchio valore.
Per aggiornare solo una parte di un oggetto mantenendo inalterato il resto, in genere si utilizza la sintassi di spread dell'oggetto. Per un oggetto di stato del modulo come { firstName, lastName, email }, potresti gestire le modifiche di input con qualcosa come setPerson({ ...person, : event.target.value })Questo copia le vecchie proprietà, quindi sovrascrive solo quella che è cambiata. La propagazione è superficiale, quindi gli oggetti annidati richiedono maggiore attenzione.
Gli oggetti annidati in profondità possono rapidamente generare codice di aggiornamento prolisso, perché è necessario creare nuove copie a ogni livello del percorso che si sta modificando. Per esempio, se person.artwork.city cambiamenti che faresti setPerson({ ...person, artwork: { ...person.artwork, city: 'London' } })Dietro le quinte, non esiste un "oggetto annidato"; ci sono oggetti separati che puntano l'uno all'altro, quindi se più oggetti padre puntano allo stesso oggetto figlio e lo si modifica, si cambiano i dati in più punti contemporaneamente.
Se ti ritrovi a scrivere continuamente layout annidati, potresti considerare di appiattire la forma del tuo layout o di utilizzare una libreria di supporto come Immer. Immer ti permette di scrivere codice che sembra mutativo (come draft.artwork.city = 'London') mentre produce una nuova copia immutabile per te dietro le quinte. In React, puoi abbinare Immer con Hooks tramite useImmer dal use-immer pacchetto.
Stato in pratica: moduli, timer e input dell'utente
Nelle applicazioni reali, raramente si gestisce lo stato solo per i contatori; si gestiscono l'input dell'utente, le risposte delle API e le "modalità" dell'interfaccia utente come caricamento, errore e successo. Il cambiamento di mentalità fondamentale con React è che non si "manipola il DOM" (ad esempio, "disabilita questo pulsante"); piuttosto, si descrive come dovrebbe apparire l'interfaccia utente per ogni stato e poi si aggiorna lo stato.
Ad esempio, un componente quiz o modulo potrebbe tenere traccia di un status stato che commuta tra 'typing', 'submitting' e 'success'. Il codice JSX disabilita condizionalmente il pulsante di invio durante l'invio del modulo e mostra un messaggio di successo una volta che la risposta è corretta. Non si chiamano mai metodi DOM imperativi: React si limita a eseguire nuovamente il rendering con il nuovo stato e l'output visivo si aggiorna di conseguenza.
La gestione dei campi dei moduli è dove molti sviluppatori incontrano per la prima volta la differenza tra la fusione dello stato della classe e useState comportamento. In classe, setState unisce l'oggetto che passi all'oggetto di stato esistente, quindi l'aggiornamento di un campo non rimuove gli altri. Con useState, gli aggiornamenti sostituiscono l'intero valore: se il tuo stato è un oggetto e chiami setState({ email: '...' }), qualsiasi altra proprietà (come password) scompaiono a meno che non vengano uniti manualmente.
Questa differenza crea difficoltà quando si effettua il refactoring passando da più variabili di stato primitive a un singolo oggetto. Se cambi da const e const a const e poi scrivi un generico setForm({ : value }), ti ritroverai con un oggetto di stato che avrà sempre e solo un campo. La soluzione è espandere l'oggetto precedente: setForm({ ...form, : value }).
Nelle app più complesse, spesso non chiamerai setState (o setSomething) direttamente da qualsiasi luogo. Potresti centralizzare lo stato utilizzando librerie come Redux o MobX, oppure utilizzare il useReducer Punto di aggancio per le macchine a stati a livello di componente. In queste configurazioni, si applicano comunque gli stessi principi di immutabilità; l'unica differenza sta nel luogo e nelle modalità di esecuzione degli aggiornamenti.
Re-rendering, prestazioni e quando utilizzare useRef
In React, ogni aggiornamento di stato innesca un nuovo rendering del componente che gestisce lo stato e, per impostazione predefinita, di tutti i suoi figli. Questo comportamento è voluto: il re-rendering è il modo in cui l'interfaccia utente rimane sincronizzata con i dati correnti. Tuttavia, ciò significa anche che un posizionamento inadeguato dello stato può causare un lavoro superfluo e rallentamenti dell'interfaccia utente, soprattutto quando i componenti figlio eseguono calcoli complessi o visualizzano elenchi di grandi dimensioni.
Immaginate un'app con un campo di input e un componente separato che mostra un lungo elenco di competenze. Se il componente padre possiede sia il testo che l'utente sta digitando sia l'elenco stesso, ogni tasto premuto comporterà il rendering dell'intero albero, incluso l'elenco delle competenze, anche se quest'ultimo non è cambiato. Questo rappresenta uno spreco di risorse.
Un modo semplice per ottimizzare questo è racchiudere i componenti figli in React.memo. React.memo è un componente di ordine superiore che memorizza il risultato di un componente funzionale: se le sue props sono le stesse tra i rendering, React salta il re-rendering. Quindi il tuo componente elenco competenze, una volta racchiuso in React.memo, non verrà ri-renderizzato ad ogni pressione di un tasto, solo quando skills La proprietà cambia effettivamente (ad esempio, quando aggiungi una nuova abilità).
Non tutti i dati “simili allo stato” appartengono a useState; A volte useRef è lo strumento migliore. Migliori useRef Hook ti dà un oggetto mutabile con un current proprietà che persiste per tutta la durata del componente, ma aggiornarla non non è un Attiva un nuovo rendering. Questo lo rende perfetto per memorizzare elementi come ID di timer, riferimenti a elementi DOM o contatori che si desidera monitorare ma che non è necessario visualizzare nell'interfaccia utente.
Un semplice esempio è un contatore implementato con useRef invece di useState. Se memorizzi il conteggio in countRef.current e incrementandolo in un gestore di eventi, il valore interno cambia, ma il JSX visualizzato non si aggiorna perché React non esegue un nuovo rendering. Questo illustra la differenza cruciale: useState è destinato ai valori che guidano l'interfaccia utente; useRef serve per i valori che si desidera conservare senza influire sul rendering.
Immutabilità e perché la mutazione diretta è una trappola
Un principio fondamentale di React è che gli aggiornamenti di stato devono essere immutabili. Questo non significa che non si possa mai cambiare nulla; significa che, invece di modificare i valori esistenti (soprattutto oggetti e array), se ne creano di nuovi e si lasciano quelli vecchi come istantanee storiche dell'interfaccia utente.
Modificare direttamente lo stato interrompe la connessione tra il tuo modello mentale e ciò che React sta facendo. Se fai qualcosa del genere state.count++ Se inserisci i dati direttamente in un array di stato, React non si accorgerà che qualcosa è cambiato perché non hai mai chiamato la funzione di aggiornamento. Lo snapshot interno che React usa per decidere quando eseguire il rendering rimane invariato, mentre il tuo codice pensa che il valore sia cambiato. È così che si verificano i bug che si "risolvono da soli" al ricaricamento della pagina.
È inoltre necessario evitare di assegnare un valore di stato a un'altra variabile e poi modificare tale variabile. Ad esempio, facendo const newCount = count; newCount++; continua a mutare lo stesso valore sottostante per i primitivi e per gli oggetti, const copy = stateObj; non crea affatto una copia, crea solo un altro riferimento allo stesso oggetto. Una copia corretta richiede modelli come { ...stateObj } per oggetti o per gli array.
Librerie come Redux, MobX (quando configurate per l'immutabilità) o Immer esistono in parte per imporre o semplificare i modelli di immutabilità. Sia che si utilizzino gli Hook integrati di React, sia una libreria per la gestione dello stato, la regola d'oro rimane valida: non modificare mai lo stato esistente se ti aspetti che React rilevi la modifica e lo rigeneri.
Aggiornamenti asincroni, raggruppamento e stato obsoleto
Un dettaglio sottile ma cruciale riguardo allo stato di React è che gli aggiornamenti sono asincroni e programmati, non applicati immediatamente. Quando chiami setState o un dispositivo di posizionamento dell'amo come setCountReact "mette in coda" un nuovo rendering per un momento futuro. Non blocca immediatamente l'aggiornamento e il rendering del codice, consentendo a React di raggruppare più aggiornamenti e mantenere prestazioni fluide.
Questo modello di pianificazione implica che non è possibile fare affidamento sulla lettura dello stato immediatamente dopo aver chiamato l'aggiornamento all'interno dello stesso blocco sincrono. Il valore che otterrai sarà in genere l'istantanea precedente. Dovresti invece pensare all'aggiornamento come a una richiesta: "la prossima volta che esegui il rendering, usa questo valore (o questa funzione di trasformazione)".
Ciò è particolarmente importante quando si aggiorna lo stato in base al suo valore corrente dall'interno di closure come setTimeout o richiamate di abbonamento. Quelle callback catturano qualunque fosse lo stato al momento della loro creazione. Se poi fai setCount(count + 1) all'interno di un timeout, il count Le informazioni a cui ti riferisci potrebbero essere obsolete nel momento in cui la funzione di callback viene effettivamente eseguita.
Questo fenomeno è noto come “stato di stallo” o “chiusure obsolete”. Ad esempio, se hai un pulsante che, al clic, chiama una funzione che imposta un timeout e poi incrementa lo stato dopo un secondo, più clic rapidi potrebbero non incrementare correttamente lo stato. Ogni callback di timeout utilizza il vecchio count ha registrato il momento in cui è stato programmato il timeout.
La soluzione più efficace consiste nell'utilizzare la forma di aggiornamento funzionale del metodo di impostazione dello stato. Invece di setCount(count + 1) all'interno del timeout, scrivi setCount(prevCount => prevCount + 1)Ora ogni callback riceve l'ultimo valore precedente al momento dell'applicazione dell'aggiornamento, non quello che si trovava nell'ambito quando è stato creato il timeout. Questo elimina il problema dello stato obsoleto senza modificare in altro modo il comportamento delle closure.
La documentazione di React evidenzia anche un dettaglio meno noto: se il tuo aggiornatore funzionale non restituisce nulla (undefined), React salterà il re-rendering. Ciò significa che le tue funzioni di aggiornamento dovrebbero sempre restituire il valore di stato successivo (o riutilizzare quello precedente) a meno che tu non voglia esplicitamente impedire un aggiornamento, cosa che raramente è desiderabile con lo standard useState utilizzo.
Comprendere questa combinazione di pianificazione asincrona, raggruppamento e comportamento delle closure è fondamentale per scrivere una logica di stato affidabile in applicazioni che gestiscono timeout, intervalli, sottoscrizioni o interazioni rapide con l'utente. Una volta compreso che i metodi che impostano lo stato pianificano gli aggiornamenti anziché eseguirli immediatamente, i bug che prima sembravano casuali inizieranno ad avere un senso.
Quando metti insieme tutte queste idee—props vs state, cicli di vita delle classi vs Hooks, immutabilità, componenti controllati, useRef Per valori non visivi, memorizzazione, aggiornamenti asincroni e closure obsolete, si ottiene un modello potente e prevedibile di come le interfacce utente React si evolvono nel tempo. Invece di pensare in termini di modifiche imperative al DOM, si progettano modelli di stato chiari e si lascia che React gestisca il re-rendering, il che rende i componenti più facili da comprendere, testare ed estendere man mano che l'applicazione cresce.

