Menu

Hook useReducer w React: składnia, przykłady, a useState

useReducer przechowuje stan i przenosi każdą aktualizację do jednej funkcji reduktora: wysyłasz akcję, a reduktor zwraca następny stan. Poznaj składnię, pisanie czystego reduktora, przykład listy zadań, leniwą inicjalizację i to, kiedy wybrać go zamiast useState.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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
}
  • reducer to funkcja (state, action) => nextState. Zdefiniuj ją poza komponentem; nie potrzebuje niczego z zasięgu komponentu.
  • initialArg to stan początkowy, używany tylko przy pierwszym renderowaniu.
  • init jest opcjonalne. Jeśli je przekażesz, stanem początkowym będzie zamiast tego init(initialArg).
  • state to 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.

useStateuseReducer
Logika aktualizacjiW każdym handlerze zdarzeniaW jednej funkcji reduktora
Dobry doKilku niezależnych wartościKilku pól zmienianych razem przez wiele zdarzeń
Ilość koduMniej przy prostym stanieWięcej na start, mniej, gdy zdarzeń przybywa
TestowanieTesty przez komponentTest reduktora jako zwykłej funkcji
DebugowanieSzukanie handlera, który ustawił stanLogowanie 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ