Dzisiejszy wpis opowie nam trochę o useReducer(), który może być używany zamiast useState(), aby zarządzać stanem komponentu. Z reguły w komponentach funkcyjnych używamy useState(), z racji tego, że useState() jest łatwym w użyciu hookiem. Zaczynając zabawę z Reactem, komponentami funkcyjnymi i hookami, zapewne w pierwszej kolejności używamy też useState().

Najpierw zacznijmy od prostego przykładu, aby pokazać jak wygląda kod, a później spróbuję przedstawić pewne plusy użycia useReducer().

W przykładzie będziemy tworzyć komponent z dwoma przyciskami, jeden przycisk będzie przypisywał losową wartość, wygenerowaną przez Math.random(), drugi będzie czyścił wartość dla stanu text.

Prosty przykład z useState():

function App() {
  const [text, setText] = React.useState(Math.random());
  
  return (
    <div>
        Text: {text}
        <br />
        <button onClick={() => setText(Math.random())}>Generate random text</button>
        <button onClick={() => setText('')}>Remove text</button>
    </div>
  );
}

Podobny przykład z useReducer():

function textReducer(state, action) {
    switch (action.type) {
        case 'generate':
            return Math.random();
        case 'remove':
            return '';
    }
}

function App() {
    const [text, dispatch] = React.useReducer(textReducer, Math.random());

    return (
        <div>
            Text: {text}
            <br />
            <button onClick={() => dispatch({type: 'generate'})}>Generate random text</button>
            <button onClick={() => dispatch({type: 'remove'})}>Remove text</button>
        </div>
    );
}

Jak widać, w przypadku useReducer() tego kodu jest trochę więcej, ale istnieją przypadki, dla których warto rozważyć użycie useReducer() zamiast useState().

Budowa useReducer()

Wróćmy jeszcze do useReducer() i przyjrzyjmy się, w jaki sposób zbudowany jest sam hook.

const [text, dispatch] = React.useReducer(reducerFunction, initialValue);

useReducer() przyjmuje jako pierwszy argument funkcję reducera, jako drugi parametr otrzymuje wartość początkową. Zwraca natomiast stan oraz dispatch. Jest to koncept bardzo podobny do Redux. (https://redux.js.org/).

Po kliknięciu w przycisk, poprzez dispatch() z odpowiednim type (nazwa dowolna, natomiast warto trzymać się pewnej konwencji), uruchamiany jest fragment logiki wewnątrz textReducer(). Finalnie zwracany jest nowy stan.

Co zyskujemy dzięki użyciu useReducer()?

Reużywalność logiki reducera

Przede wszystkim dzięki wydzieleniu części odpowiadającej za modyfikację tekstu do zewnętrznej funkcji textReducer(state, action) możemy używać jej w innych komponentach. Wynosimy funkcję gdzieś na zewnątrz i importujemy w komponentach, które mają używać podobnej logiki.

function KomponentPierwszy() {
    const [text, dispatch] = React.useReducer(textReducer, Math.random());
    ....
}

function KomponentPierwszy() {
    const [someText, dispatch] = React.useReducer(textReducer, Math.random());
    ....
}
Zmiany tylko przez zdefiniowaną logikę i funkcję reducera

Kolejny plus (tutaj znów możemy odnieść się do Redux). Aby zmienić stan dla text, musimy użyć funkcji reducera. Co to oznacza?
Zmiana zachodzi tylko poprzez funkcję. Nie powinno być zatem sytuacji, że zmienimy zawartość text w dowolnym dla nas miejscu. W ten sposób łatwiej debugować aplikację i zarządzać stanem komponentu w świadomy sposób. W funkcji bowiem, zdefiniowaną mamy logikę, która jest właściwa dla logiki komponentu.

Jeżeli nadal to co napisałem wyżej jest niejasne, posłużę się szybkim przykładem:

Używając useState(text, setText), możemy przekazać do stanu text dowolną wartość. Załóżmy, że chcemy generować tylko liczbę, poprzez Math.random() i nie dopuszczamy innej wartości. W przypadku użycia useState() musimy sami zadbać o to, aby gdzieś w komponencie nie zrobić np. setText('STRING'), przypisując błędną wartość. Korzystając z useReducer() i naszej funkcji textReducer() sprawa jest załatwiona z automatu, bowiem wywołujemy tylko zdefiniowane fragmenty logiki poprzez dispatch({type: ...})

Nazwanie złożonej logiki

Jedno z podejść w programowaniu mówi nam, aby zmienne, metody itd. były nazywane w taki sposób aby dało się z tego wyczytać również logikę aplikacji… Dzięki zastosowaniu funkcji reducera i odpowiednim nazwaniu action.type możemy to trochę usprawnić.

Stan jako obiekt

Rozszerzmy lekko nasz przykład i dodajmy do niego logikę, która informuje nas, czy kiedykolwiek kliknęliśmy w przycisk. Poza stanem text pojawia nam się drugi stan… Bez użycia useReducer() musielibyśmy używać dwa razy useState(), raz dla stanu text, drugi raz dla stanu buttonClicked.
W tym przypadku zmieniamy initial state na obiekt, oraz lekko modyfikujemy reducer.

function textReducer(state, action) {
    switch (action.type) {
        case 'generate':
            return {
                text: Math.random(),
                buttonClicked: true
            }
        case 'remove':
            return {
                text: '',
                buttonClicked: true
            };
    }
}

function App() {
    const [textState, dispatch] = React.useReducer(textReducer, {text: Math.random(), buttonClicked: false});

    return (
        <div>
            Text: {textState.text}
            <br />
            Button clicked: {textState.buttonClicked.toString()}
            <button onClick={() => dispatch({type: 'generate'})}>Generate random text</button>
            <button onClick={() => dispatch({type: 'remove'})}>Remove text</button>
        </div>
    );
}

Oczywiście jest to bardzo prosty przykład, ale kiedy logika będzie bardziej skomplikowana, użycie useReducer() może mieć dobre zastosowanie.

Łatwe testowanie

Przyjrzyjmy się poniższej funkcji:

function calculatorReducer(state, action) {
    switch (action.type) {
        case 'PLUS':
            return state + 1;
        case 'MINUS':
            return state - 1;
    }
}

Dla tych samych wartości wejściowych, funkcja zawsze zwróci to samo. Mamy tutaj do czynienia z czystą funkcją. W związku z tym, bardzo łatwo jest testować taką funkcję. Przekazujemy początkowy stan, wywołujemy różne akcje i sprawdzamy, czy wartość po wywołaniu jest wartością oczekiwaną, bardzo proste i przyjemne.

Przy okazji zapraszam do innego artykułu z mojego bloga, dotyczącego czystych funkcji.

Zależność od innej wartości stanu

Wyobraźmy sobie, że logika w naszym komponencie zależy od kilku stanów, które w jakiś sposób na siebie oddziałują. Na szybko przerobiłem funkcję reducera, aby pokazać z czym mamy do czynienia. Załóżmy, że możemy klikać w nasz przycisk, ale nie więcej niż 10 razy. Mamy zatem dwa stany, jeden przechowujący aktualny tekst oraz drugi, który liczy ilość kliknięć w przycisk.

function textReducer(state, action) {
    switch (action.type) {
        case 'generate':
            if (state.clickCount == 10) {
              return state;
            }

            return {
                clickCount: state.clickCount + 1,
                text: Math.random()
            }
        case 'remove':
            return {
                ...state,
                text: ''
            };
    }
}

W przypadku użycia useState() mielibyśmy zdefiniowane zapewne dwa stany oraz handler, w którym byłaby obsługiwana logika.

Wady

Na pewno pierwszą wadą jest ilość kodu, który musimy napisać. Czasami mamy bardzo proste komponenty, gdzie użycie useState() będzie intuicyjne i po prostu… lepsze. Może to być chociażby jakaś flaga na zasadzie loading lub jakiś inny pojedynczy stan. W takim przypadku nie ma sensu na siłę używać useReducer().
Osobiście skłaniałbym się do używania useReducer() w bardziej skomplikowanej logice. Również można rozważyć użycie useReducer(), gdy wartość stanu w komponencie zależy od innego stanu tego samego komponentu, co pokazałem w przykładzie z licznikiem kliknięć i blokadą zmiany stanu.

Pamiętajmy jednak, że skomplikowana logika może świadczyć o tym, że nasz komponent jest tą logiką „przeładowany” i może warto pomyśleć nad innym podziałem kodu.

Dzięki za Twój czas. Mam nadzieję, że artykuł zainspirował Ciebie do czegoś i coś z niego wyniosłeś/aś, bowiem useReducer() jest rzadziej używanym hookiem niż useState(). 🙂

Źródła:
Obraz: https://unsplash.com/photos/wkieEIVb1pA
Hooki na stronie React: https://pl.reactjs.org/docs/hooks-intro.html
Redux: https://redux.js.org/basics/usage-with-react