useReducer
useReducer è un Hook React che ti permette di aggiungere un reducer al tuo componente.
const [state, dispatch] = useReducer(reducer, initialArg, init?)- Reference
- Usage
- Troubleshooting
- Ho eseguito il dispatch di un’azione, ma il log mi mostra il vecchio valore di state
- Ho eseguito il dispatch di un’azione, ma lo schermo non si aggiorna
- Una parte dello state del mio reducer diventa undefined dopo il dispatch
- Tutto lo state del mio reducer diventa undefined dopo il dispatch
- Ricevo un errore: “Too many re-renders”
- La mia funzione reducer o di inizializzazione viene eseguita due volte
Reference
useReducer(reducer, initialArg, init?)
Chiama useReducer al top level del tuo componente per gestire il suo state con un reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...Parameters
reducer: La funzione reducer che specifica come lo state viene aggiornato. Deve essere pura, deve accettare lo state e l’azione come argomenti e deve restituire il prossimo state. State e azione possono essere di qualsiasi tipo.initialArg: Il valore da cui viene calcolato lo state iniziale. Può essere un valore di qualsiasi tipo. Come lo state iniziale viene calcolato da esso dipende dal successivo argomentoinit.- optional
init: La funzione di inizializzazione che deve restituire lo state iniziale. Se non è specificata, lo state iniziale è impostato suinitialArg. Altrimenti, lo state iniziale è impostato sul risultato della chiamata ainit(initialArg).
Returns
useReducer restituisce un array con esattamente due valori:
- Lo state corrente. Durante la renderizzazione iniziale, è impostato su
init(initialArg)oinitialArg(se non c’èinit). - La funzione
dispatchche ti permette di aggiornare lo state a un valore diverso e avviare una ri-renderizzazione.
Caveats
useReducerè un Hook, quindi puoi chiamarlo solo al top level del tuo componente o dei tuoi Hook. Non puoi chiamarlo all’interno di loop o condizioni. Se ne hai bisogno, estrai un nuovo componente e sposta lo state al suo interno.- La funzione
dispatchha un’identità stabile, quindi spesso la vedrai omessa dalle dipendenze degli Effetti, ma includerla non farà scattare l’Effetto. Se il linter ti permette di omettere una dipendenza senza errori, è sicuro farlo. Scopri di più sulla rimozione delle dipendenze degli Effetti. - In Strict Mode, React chiamerà il tuo reducer e la funzione di inizializzazione due volte per aiutarti a trovare impurità accidentali. Questo è un comportamento solo in development e non influisce sulla production. Se il tuo reducer e la funzione di inizializzazione sono puri (come dovrebbero essere), non dovrebbe influire sulla tua logica. Il risultato di una delle chiamate viene ignorato.
Funzione dispatch
La funzione dispatch restituita da useReducer ti permette di aggiornare lo state a un valore diverso e avviare una ri-renderizzazione. Devi passare l’azione come unico argomento alla funzione dispatch:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...React imposterà il prossimo state sul risultato della chiamata alla funzione reducer che hai fornito con lo state corrente e l’azione che hai passato a dispatch.
Parameters
action: L’azione eseguita dall’utente. Può essere un valore di qualsiasi tipo. Per convenzione, un’azione è solitamente un oggetto con una proprietàtypeche la identifica e, opzionalmente, altre proprietà con informazioni aggiuntive.
Returns
Le funzioni dispatch non hanno un valore di ritorno.
Caveats
-
La funzione
dispatchaggiorna la variabile di state solo per la prossima renderizzazione. Se leggi la variabile di state dopo aver chiamato la funzionedispatch, otterrai comunque il vecchio valore che era sullo schermo prima della tua chiamata. -
Se il nuovo valore che fornisci è identico allo
statecorrente, come determinato da un confrontoObject.is, React salterà la ri-renderizzazione del componente e dei suoi figli. Si tratta di un’ottimizzazione. React potrebbe comunque dover chiamare il tuo componente prima di ignorare il risultato, ma non dovrebbe influire sul tuo codice. -
React raggruppa gli aggiornamenti di state. Aggiorna lo schermo dopo che tutti i gestori di eventi sono stati eseguiti e hanno chiamato le loro funzioni
set. Questo previene più ri-renderizzazioni durante un singolo evento. Nel raro caso in cui devi forzare React ad aggiornare lo schermo prima, per esempio per accedere al DOM, puoi usareflushSync.
Usage
Aggiungere un reducer a un componente
Chiama useReducer al top level del tuo componente per gestire lo state con un reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...useReducer restituisce un array con esattamente due elementi:
- Lo state corrente di questa variabile di state, inizialmente impostato sullo state iniziale che hai fornito.
- La funzione
dispatchche ti permette di cambiarlo in risposta all’interazione.
Per aggiornare ciò che appare sullo schermo, chiama dispatch con un oggetto che rappresenta ciò che l’utente ha fatto, chiamato azione:
function handleClick() {
dispatch({ type: 'incremented_age' });
}React passerà lo state corrente e l’azione alla tua funzione reducer. Il tuo reducer calcolerà e restituirà il prossimo state. React memorizzerà quel prossimo state, renderizzerà di nuovo il tuo componente con esso e aggiornerà l’UI.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Increment age </button> <p>Hello! You are {state.age}.</p> </> ); }
useReducer è molto simile a useState, ma ti permette di spostare la logica di aggiornamento dello state dai gestori di eventi in una singola funzione fuori dal tuo componente. Leggi di più su scegliere tra useState e useReducer.
Scrivere la funzione reducer
Una funzione reducer è dichiarata così:
function reducer(state, action) {
// ...
}Poi devi completare il codice che calcolerà e restituirà il prossimo state. Per convenzione, è comune scriverlo come uno switch statement. Per ogni case nello switch, calcola e restituisci un prossimo state.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}Le azioni possono avere qualsiasi forma. Per convenzione, è comune passare oggetti con una proprietà type che identifica l’azione. Dovrebbe includere le informazioni minime necessarie affinché il reducer possa calcolare il prossimo state.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...I nomi dei tipi di azione sono locali al tuo componente. Ogni azione descrive una singola interazione, anche se ciò porta a più cambiamenti nei dati. La forma dello state è arbitraria, ma di solito sarà un oggetto o un array.
Leggi Estrarre la Logica dello State in un Reducer per saperne di più.
Esempio 1 di 3: Form (oggetto)
In questo esempio, il reducer gestisce un oggetto di state con due campi: name e age.
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); } const initialState = { name: 'Taylor', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> Increment age </button> <p>Hello, {state.name}. You are {state.age}.</p> </> ); }
Evitare di ricreare lo state iniziale
React salva lo state iniziale una volta e lo ignora nelle renderizzazioni successive.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...Sebbene il risultato di createInitialState(username) venga usato solo per la renderizzazione iniziale, stai comunque chiamando questa funzione a ogni renderizzazione. Questo può essere inefficiente se crea array grandi o esegue calcoli costosi.
Per risolvere, puoi passarla come funzione di inizializzazione a useReducer come terzo argomento:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...Nota che stai passando createInitialState, cioè la funzione stessa, e non createInitialState(), che è il risultato della sua chiamata. In questo modo, lo state iniziale non viene ricreato dopo l’inizializzazione.
Nell’esempio sopra, createInitialState accetta un argomento username. Se la tua funzione di inizializzazione non ha bisogno di informazioni per calcolare lo state iniziale, puoi passare null come secondo argomento a useReducer.
Esempio 1 di 2: Passare la funzione di inizializzazione
Questo esempio passa la funzione di inizializzazione, quindi la funzione createInitialState viene eseguita solo durante l’inizializzazione. Non viene eseguita quando il componente viene renderizzato di nuovo, ad esempio quando digiti nell’input.
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: username + "'s task #" + (i + 1) }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Unknown action: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>Add</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Troubleshooting
Ho eseguito il dispatch di un’azione, ma il log mi mostra il vecchio valore di state
Chiamare la funzione dispatch non cambia lo state nel codice in esecuzione:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Richiede una ri-renderizzazione con 43
console.log(state.age); // Ancora 42!
setTimeout(() => {
console.log(state.age); // Anche 42!
}, 5000);
}Questo perché lo state si comporta come un’istantanea. Aggiornare lo state richiede un’altra renderizzazione con il nuovo valore di state, ma non influisce sulla variabile JavaScript state nel gestore di eventi già in esecuzione.
Se devi indovinare il prossimo valore di state, puoi calcolarlo manualmente chiamando tu stesso il reducer:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }Ho eseguito il dispatch di un’azione, ma lo schermo non si aggiorna
React ignorerà il tuo aggiornamento se il prossimo state è uguale al precedente, come determinato da un confronto Object.is. Di solito succede quando modifichi direttamente un oggetto o un array nello state:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Sbagliato: mutare l'oggetto esistente
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Sbagliato: mutare l'oggetto esistente
state.name = action.nextName;
return state;
}
// ...
}
}Hai mutato un oggetto state esistente e l’hai restituito, quindi React ha ignorato l’aggiornamento. Per correggere, devi assicurarti di Aggiornare gli Oggetti nello State e Aggiornare gli Array nello State invece di mutarli:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Corretto: creare un nuovo oggetto
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Corretto: creare un nuovo oggetto
return {
...state,
name: action.nextName
};
}
// ...
}
}Una parte dello state del mio reducer diventa undefined dopo il dispatch
Assicurati che ogni ramo case copi tutti i campi esistenti quando restituisce il nuovo state:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Non dimenticarlo!
age: state.age + 1
};
}
// ...Senza ...state sopra, il prossimo state restituito conterrebbe solo il campo age e nient’altro.
Tutto lo state del mio reducer diventa undefined dopo il dispatch
Se il tuo state diventa inaspettatamente undefined, probabilmente stai dimenticando di return state in uno dei casi, oppure il tuo tipo di azione non corrisponde a nessuno dei case. Per scoprire perché, lancia un errore fuori dallo switch:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}Puoi anche usare un type checker statico come TypeScript per individuare questi errori.
Ricevo un errore: “Too many re-renders”
Potresti ricevere un errore che dice: Too many re-renders. React limits the number of renders to prevent an infinite loop. Di solito, significa che stai eseguendo incondizionatamente un dispatch di un’azione durante la renderizzazione, quindi il tuo componente entra in un loop: renderizzazione, dispatch (che causa una renderizzazione), renderizzazione, dispatch (che causa una renderizzazione), e così via. Molto spesso, questo è causato da un errore nella specifica di un gestore di eventi:
// 🚩 Sbagliato: chiama il gestore durante la renderizzazione
return <button onClick={handleClick()}>Click me</button>
// ✅ Corretto: passa il gestore di eventi
return <button onClick={handleClick}>Click me</button>
// ✅ Corretto: passa una funzione inline
return <button onClick={(e) => handleClick(e)}>Click me</button>Se non riesci a trovare la causa di questo errore, clicca sulla freccia accanto all’errore nella console e scorri lo stack JavaScript per trovare la specifica chiamata alla funzione dispatch responsabile dell’errore.
La mia funzione reducer o di inizializzazione viene eseguita due volte
In Strict Mode, React chiamerà il tuo reducer e le funzioni di inizializzazione due volte. Non dovrebbe rompere il tuo codice.
Questo comportamento solo in development ti aiuta a Mantenere i Componenti Puri. React usa il risultato di una delle chiamate e ignora il risultato dell’altra. Finché il tuo componente, la funzione di inizializzazione e il reducer sono puri, non dovrebbe influire sulla tua logica. Tuttavia, se sono accidentalmente impuri, questo ti aiuta a notare gli errori.
Per esempio, questa funzione reducer impura muta un array nello state:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Errore: mutare lo state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}Poiché React chiama la tua funzione reducer due volte, vedrai che il todo è stato aggiunto due volte, quindi saprai che c’è un errore. In questo esempio, puoi correggere l’errore sostituendo l’array invece di mutarlo:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Corretto: sostituire con un nuovo state
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}Ora che questa funzione reducer è pura, chiamarla una volta in più non fa differenza nel comportamento. Ecco perché React chiamarla due volte ti aiuta a trovare gli errori. Solo le funzioni del componente, di inizializzazione e reducer devono essere pure. I gestori di eventi non devono essere puri, quindi React non chiamerà mai i tuoi gestori di eventi due volte.
Leggi Mantenere i Componenti Puri per saperne di più.