Menu

L'hook useReducer di React: sintassi, esempi, vs useState

useReducer memorizza lo stato e sposta ogni aggiornamento in un'unica funzione reducer: invii un'azione con dispatch, e il reducer restituisce lo stato successivo. Impara la sintassi, come scrivere un reducer puro, un esempio di lista di cose da fare, l'inizializzazione lazy e quando sceglierlo al posto di useState.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

useReducer è un hook di React che tiene lo stato come useState, ma sposta ogni modo di cambiarlo in un'unica funzione, il reducer. Il tuo componente chiama dispatch con un oggetto azione come { type: 'increment' }, e React passa lo stato attuale e quell'azione al reducer, che restituisce lo stato successivo.

I pulsanti non dicono come cambia il conteggio, solo cosa è successo. Tutta l'aritmetica vive in reducer. Aggiungi un case 'double' che restituisce { count: state.count * 2 } e un pulsante che lo invia con dispatch per vedere come si inserisce un nuovo tipo di aggiornamento.

La sintassi

const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional

function reducer(state, action) {
    // return the next state
}
  • reducer è una funzione (state, action) => nextState. Definiscila fuori dal componente; non ha bisogno di nulla dallo scope del componente.
  • initialArg è lo stato iniziale, usato solo al primo rendering.
  • init è facoltativa. Se la passi, lo stato iniziale diventa init(initialArg).
  • state è lo stato attuale per questo rendering.
  • dispatch(action) invia un'azione al reducer e programma un rendering con il risultato. È stabile: è la stessa funzione a ogni rendering, quindi è sicuro passarla verso il basso o elencarla come dipendenza.

Un'azione può essere qualsiasi valore, ma per convenzione è un oggetto con una stringa type che descrive cosa è successo, più i dati di cui il reducer ha bisogno: { type: 'added', text: 'Buy milk' }.

Scrivere un reducer

La maggior parte dei reducer è uno switch su action.type, con un case per ogni tipo di evento. Ogni case restituisce uno stato completamente nuovo; non modifica mai quello vecchio. Il case default genera un errore, così un refuso come dispatch({ type: 'incremnet' }) fallisce in modo evidente invece di non fare nulla.

Dai alle azioni il nome di ciò che ha fatto l'utente (added, toggled, deleted), non della modifica di stato che hai in mente (setTodos). Il componente si legge allora come una lista di eventi, e il reducer è l'unico punto che decide cosa significa ogni evento.

I reducer devono essere puri

Un reducer è una funzione pura: dati lo stesso stato e la stessa azione, restituisce lo stesso risultato, e non fa nient'altro. Questo significa niente mutazioni, niente richieste, niente timer, niente Math.random() o Date.now() al suo interno. React fa affidamento su questo. In sviluppo con StrictMode, React chiama il tuo reducer due volte per ogni azione e tiene un solo risultato, per aiutarti a notare un reducer che non è puro. L'anteprima qui gira come una build di produzione, quindi gli esempi di questa pagina lo chiamano una volta sola.

La regola della mutazione è quella che si infrange più spesso. Fare push in state.todos e restituire state restituisce lo stesso oggetto, quindi React non vede alcun cambiamento e salta il rendering, la stessa trappola descritta in aggiornare array e oggetti. Costruisci nuovi array e oggetti con spread, map e filter.

// Wrong: mutates and returns the same object
case 'added':
    state.todos.push(action.todo);
    return state;

// Right: returns a new object with a new array
case 'added':
    return { ...state, todos: [...state.todos, action.todo] };

Gli effetti collaterali che appartengono a un evento, come salvare su un server, vanno nel gestore di eventi accanto a dispatch. Quelli che appartengono allo stato, come sincronizzarlo con localStorage, vanno in un effetto.

Una lista di cose da fare con useReducer

Ecco il reducer al lavoro sul serio: aggiungere, spuntare ed eliminare attività. Il console.log in cima al reducer stampa ogni azione, un modo pratico per osservare come cambia lo stato mentre fai debug. È l'unico effetto collaterale comunemente tollerato in un reducer, perché non cambia nulla; toglilo prima di pubblicare.

Aggiungi un'attività, spunta una casella ed eliminane una, poi leggi la console: ogni modifica è una riga che nomina l'azione e i suoi dati. Il testo dell'input resta in useState, perché è locale al campo e nessun altro evento lo tocca. Combinare i due hook in un componente è normale.

L'id della nuova attività viene creato nel gestore del clic e viaggia nell'azione, quindi il reducer si limita a copiarlo. Scrivere id: nextId++ dentro il reducer lo renderebbe impuro: con StrictMode in sviluppo la seconda chiamata salterebbe un id.

dispatch non cambia subito lo stato

dispatch funziona come un setter di useState: programma un rendering, e la variabile state nel tuo gestore attuale mantiene il vecchio valore. Per usare subito il nuovo stato, calcolalo tu chiamando il reducer.

Clicca qualche volta. La prima riga del log è sempre indietro di un passo, e la seconda corrisponde al numero sul pulsante dopo il rendering. Poiché il reducer è una semplice funzione, chiamarlo tu è sicuro, un altro vantaggio del mantenerlo puro.

useState vs useReducer

Entrambi gli hook memorizzano stato, e tutto ciò che scrivi con uno puoi scriverlo con l'altro. La differenza è dove vive la logica di aggiornamento.

useStateuseReducer
Logica di aggiornamentoIn ogni gestore di eventiIn un'unica funzione reducer
Adatto aPochi valori indipendentiPiù campi cambiati insieme da molti eventi
Quantità di codiceMeno per uno stato sempliceDi più all'inizio, di meno man mano che gli eventi crescono
TestTesti tramite il componenteTesti il reducer come semplice funzione
DebugTrovi quale gestore l'ha impostatoRegistri ogni azione in un solo punto

Scegli useReducer quando noti che lo stesso stato viene aggiornato in molti gestori, quando un evento deve cambiare diversi pezzi di stato che devono restare coerenti, o quando un gestore è per lo più logica sullo stato successivo. Per un interruttore o un campo di testo, useState è più breve e più chiaro. Cambiare in seguito non è difficile: sostituisci i setter con dei dispatch e sposta la logica nei case.

Inizializzazione lazy

Se scrivi lo stato iniziale come chiamata di funzione, come useReducer(reducer, createInitialState('Ada')), quella chiamata viene eseguita a ogni rendering, anche se React usa il suo risultato solo la prima volta. Se costruire lo stato iniziale è costoso, o deve essere derivato da una prop, passa un terzo argomento: una funzione init. React chiama init(initialArg) una sola volta, al primo rendering.

Scrivi nell'input: il componente si renderizza a ogni tasto, ma createInitialState registra un solo log. Ora cambia la chiamata in useReducer(reducer, createInitialState('Ada')) e scrivi di nuovo. La funzione viene eseguita a ogni rendering, e dopo il primo React butta via il risultato ogni volta.

Passa la funzione stessa, non il risultato della sua chiamata. useReducer(reducer, 'Ada', createInitialState) è lazy; useReducer(reducer, createInitialState('Ada')) no.

useReducer con il context

Un reducer si abbina bene al context quando un albero profondo ha bisogno dello stato. Metti lo stato e dispatch nel context in cima, e qualsiasi componente sottostante può leggere lo stato o inviare azioni senza props passate attraverso ogni livello. Poiché dispatch non cambia mai, i componenti che inviano solo azioni possono leggere un context separato per dispatch e saltare il nuovo render quando lo stato cambia.

import { createContext, useContext, useReducer } from 'react';

const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);

export function TodosProvider({ children }) {
    const [todos, dispatch] = useReducer(todosReducer, []);
    return (
        <TodosContext value={todos}>
            <TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
        </TodosContext>
    );
}

function AddTodo() {
    const dispatch = useContext(TodosDispatchContext);
    return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}

In React 19 un oggetto context funziona come proprio provider (<TodosContext value={...}>); anche <TodosContext.Provider> funziona ancora. La pagina su useContext spiega come il context raggiunge i componenti e quando li fa renderizzare di nuovo.

Domande frequenti

Cos'è useReducer in React?

Un hook per uno stato i cui aggiornamenti sono descritti come azioni. Chiami const [state, dispatch] = useReducer(reducer, initialState), poi dispatch({ type: 'added' }) dai gestori di eventi. React passa al tuo reducer lo stato attuale e l'azione, e ciò che restituisce diventa lo stato successivo.

Quando dovrei usare useReducer invece di useState?

Quando diversi eventi aggiornano lo stesso stato in modi correlati, quando lo stato successivo dipende da più campi contemporaneamente, o quando la logica di aggiornamento è abbastanza lunga da volerla in un'unica funzione testabile fuori dal componente. Per uno o due valori indipendenti, useState è più semplice.

Perché un reducer deve essere puro?

React può chiamare il tuo reducer più di una volta per la stessa azione (StrictMode lo fa apposta in sviluppo) e si aspetta lo stesso risultato ogni volta. Quindi un reducer non deve modificare lo stato, recuperare dati, impostare timer o leggere valori casuali. Fai queste cose nei gestori di eventi o negli effetti.

dispatch aggiorna lo stato immediatamente?

No. Come un setter di useState, dispatch programma un rendering. Dentro il gestore di eventi attuale state contiene ancora il vecchio valore; il nuovo stato compare nel rendering successivo.

Qual è il terzo argomento di useReducer?

Una funzione init facoltativa. Quando la passi, React calcola lo stato iniziale come init(initialArg), e solo al primo rendering. È utile quando costruire lo stato iniziale è costoso o dipende da una prop.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA