useReducer to hook Reacta, który przechowuje stan jak useState, ale przenosi każdy sposób jego zmiany do jednej funkcji, reduktora (reducer). Twój komponent wywołuje dispatch z obiektem akcji, takim jak { type: 'increment' }, a React przekazuje bieżący stan i tę akcję do reduktora, który zwraca następny stan.
Przyciski nie mówią, jak zmienia się licznik, tylko co się stało. Cała arytmetyka znajduje się w reducer. Dodaj case 'double', który zwraca { count: state.count * 2 }, i przycisk, który go wysyła, żeby zobaczyć, jak pasuje nowy rodzaj aktualizacji.
Składnia
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reducerto funkcja(state, action) => nextState. Zdefiniuj ją poza komponentem; nie potrzebuje niczego z zasięgu komponentu.initialArgto stan początkowy, używany tylko przy pierwszym renderowaniu.initjest opcjonalne. Jeśli je przekażesz, stanem początkowym będzie zamiast tegoinit(initialArg).stateto bieżący stan dla tego renderowania.dispatch(action)wysyła akcję do reduktora i planuje renderowanie z wynikiem. Jest stabilne: to ta sama funkcja przy każdym renderowaniu, więc można ją bezpiecznie przekazywać w dół albo umieszczać na liście zależności.
Akcja może być dowolną wartością, ale zgodnie z konwencją to obiekt z tekstem type, który opisuje, co się stało, plus dane, których potrzebuje reduktor: { type: 'added', text: 'Buy milk' }.
Pisanie reduktora
Większość reduktorów to switch po action.type, z jednym case na każdy rodzaj zdarzenia. Każdy przypadek zwraca zupełnie nowy stan; nigdy nie zmienia starego. Przypadek default rzuca błąd, więc literówka taka jak dispatch({ type: 'incremnet' }) kończy się głośnym błędem, a nie cichym brakiem działania.
Nazywaj akcje od tego, co zrobił użytkownik (added, toggled, deleted), a nie od zmiany stanu, którą masz na myśli (setTodos). Komponent czyta się wtedy jak lista zdarzeń, a reduktor jest jedynym miejscem, które decyduje, co oznacza każde zdarzenie.
Reduktory muszą być czyste
Reduktor to czysta funkcja: dla tego samego stanu i akcji zwraca ten sam wynik i nie robi nic więcej. Oznacza to brak mutacji, żądań, timerów, Math.random() i Date.now() w środku. React na tym polega. W trybie deweloperskim pod StrictMode React wywołuje reduktor dwa razy dla każdej akcji i zatrzymuje jeden wynik, żeby pomóc ci zauważyć reduktor, który nie jest czysty. Podgląd tutaj działa jak build produkcyjny, więc przykłady na tej stronie wywołują go raz.
Zasadę mutacji łamie się najczęściej. Dodanie elementu przez push do state.todos i zwrócenie state zwraca ten sam obiekt, więc React nie widzi zmiany i pomija renderowanie; to ta sama pułapka, którą opisuje strona o aktualizowaniu tablic i obiektów. Buduj nowe tablice i obiekty przez spread, map i 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] };
Efekty uboczne, które należą do zdarzenia, na przykład zapis na serwerze, umieszczaj w handlerze zdarzenia obok dispatch. Efekty uboczne, które należą do stanu, na przykład synchronizacja z localStorage, umieszczaj w efekcie.
Lista zadań z useReducer
Oto reduktor wykonujący prawdziwą pracę: dodawanie, przełączanie i usuwanie zadań. console.log na początku reduktora wypisuje każdą akcję, co jest wygodnym sposobem obserwowania zmian stanu podczas debugowania. To jedyny efekt uboczny, który zwykle toleruje się w reduktorze, bo niczego nie zmienia; usuń go przed wdrożeniem.
Dodaj zadanie, zaznacz pole i usuń jedno zadanie, a potem przeczytaj konsolę: każda zmiana to jedna linia z nazwą akcji i jej danymi. Tekst pola zostaje w useState, bo jest lokalny dla pola i żadne inne zdarzenie go nie dotyka. Mieszanie obu hooków w jednym komponencie jest normalne.
ID nowego zadania powstaje w handlerze kliknięcia i podróżuje w akcji, więc reduktor tylko je kopiuje. Napisanie id: nextId++ wewnątrz reduktora uczyniłoby go nieczystym: pod StrictMode w trybie deweloperskim drugie wywołanie pominęłoby jedno ID.
Dispatch nie zmienia stanu od razu
dispatch działa jak setter z useState: planuje renderowanie, a zmienna state w bieżącym handlerze zachowuje starą wartość. Żeby od razu użyć nowego stanu, wylicz go sam, wywołując reduktor.
Kliknij kilka razy. Pierwsza linia logu jest zawsze o krok w tyle, a druga zgadza się z liczbą na przycisku po renderowaniu. Ponieważ reduktor to zwykła funkcja, samodzielne wywołanie go jest bezpieczne i to kolejna korzyść z utrzymywania go czystym.
useState a useReducer
Oba hooki przechowują stan i wszystko, co napiszesz jednym, napiszesz też drugim. Różnica polega na tym, gdzie żyje logika aktualizacji.
| useState | useReducer | |
|---|---|---|
| Logika aktualizacji | W każdym handlerze zdarzenia | W jednej funkcji reduktora |
| Dobry do | Kilku niezależnych wartości | Kilku pól zmienianych razem przez wiele zdarzeń |
| Ilość kodu | Mniej przy prostym stanie | Więcej na start, mniej, gdy zdarzeń przybywa |
| Testowanie | Testy przez komponent | Test reduktora jako zwykłej funkcji |
| Debugowanie | Szukanie handlera, który ustawił stan | Logowanie każdej akcji w jednym miejscu |
Sięgaj po useReducer, gdy zauważasz, że ten sam stan jest aktualizowany w wielu handlerach, gdy jedno zdarzenie musi zmienić kilka kawałków stanu, które muszą pozostać spójne, albo gdy handler to głównie logika wyliczania następnego stanu. Dla przełącznika albo pola tekstowego useState jest krótszy i jaśniejszy. Późniejsza zmiana nie jest trudna: zastąp settery wywołaniami dispatch i przenieś logikę do przypadków.
Leniwa inicjalizacja
Jeśli zapiszesz stan początkowy jako wywołanie funkcji, na przykład useReducer(reducer, createInitialState('Ada')), to wywołanie wykonuje się przy każdym renderowaniu, choć React używa jego wyniku tylko za pierwszym razem. Jeśli budowanie stanu początkowego jest kosztowne albo powinno wynikać z propsa, przekaż trzeci argument: funkcję init. React wywoła init(initialArg) raz, przy pierwszym renderowaniu.
Pisz w polu: komponent renderuje się przy każdym naciśnięciu klawisza, ale createInitialState wypisuje log tylko raz. Teraz zmień wywołanie na useReducer(reducer, createInitialState('Ada')) i pisz ponownie. Funkcja wykonuje się przy każdym renderowaniu, a React po pierwszym razie za każdym razem wyrzuca wynik.
Przekaż samą funkcję, a nie wynik jej wywołania. useReducer(reducer, 'Ada', createInitialState) jest leniwe; useReducer(reducer, createInitialState('Ada')) nie.
useReducer z kontekstem
Reduktor dobrze łączy się z kontekstem, gdy stanu potrzebuje głębokie drzewo. Umieść stan i dispatch w kontekście na górze, a każdy komponent niżej może odczytywać stan albo wysyłać akcje bez propsów przekazywanych przez każdą warstwę. Ponieważ dispatch nigdy się nie zmienia, komponenty, które tylko wysyłają akcje, mogą czytać osobny kontekst dispatch i nie renderować się ponownie, gdy zmienia się stan.
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>;
}
W React 19 obiekt kontekstu działa jako własny provider (<TodosContext value={...}>); <TodosContext.Provider> też nadal działa. Strona o useContext omawia, jak kontekst dociera do komponentów i kiedy renderuje je ponownie.
Najczęściej zadawane pytania
Co to jest useReducer w React?
Hook do stanu, którego aktualizacje opisuje się jako akcje. Wywołujesz const [state, dispatch] = useReducer(reducer, initialState), a potem dispatch({ type: 'added' }) z handlerów zdarzeń. React przekazuje bieżący stan i akcję do twojego reducer, a to, co zwróci, staje się następnym stanem.
Kiedy używać useReducer zamiast useState?
Gdy kilka zdarzeń aktualizuje ten sam stan w powiązany sposób, gdy następny stan zależy naraz od kilku pól albo gdy logika aktualizacji jest na tyle długa, że chcesz ją mieć w jednej testowalnej funkcji poza komponentem. Dla jednej czy dwóch niezależnych wartości useState jest prostszy.
Dlaczego reduktor musi być czysty?
React może wywołać reduktor więcej niż raz dla tej samej akcji (StrictMode robi to celowo w trybie deweloperskim) i oczekuje za każdym razem tego samego wyniku. Reduktor nie może więc mutować stanu, pobierać danych, ustawiać timerów ani odczytywać losowych wartości. Rób te rzeczy w handlerach zdarzeń albo efektach.
Czy dispatch aktualizuje stan od razu?
Nie. Tak jak setter z useState, dispatch planuje renderowanie. W bieżącym handlerze zdarzenia state wciąż ma starą wartość; nowy stan pojawia się w następnym renderowaniu.
Jaki jest trzeci argument useReducer?
Opcjonalna funkcja init. Gdy ją przekażesz, React wylicza stan początkowy jako init(initialArg) i tylko przy pierwszym renderowaniu. Przydaje się, gdy budowanie stanu początkowego jest kosztowne albo zależy od propsa.