Освоение useReducer

Узнайте о useReducer React и используйте его для создания более предсказуемых и надежных пользовательских интерфейсов!

В этом сообщении в блоге предполагается, что у вас есть некоторые знания о React и хуках React.

Управление состоянием в React

Как вы, наверное, знаете, в React есть 2 способа управления состоянием:

Оба широко используются в любом конкретном приложении React, и хотя в конечном итоге они служат одной и той же цели (управление состоянием), их следует использовать в разных ситуациях.

Когда использовать useReducer против useState

useReducer обычно предпочтительнее useState, если у вас сложная логика состояния, включающая несколько подзначений, или когда следующее состояние зависит от предыдущего.Документация React

Как указано в абзаце выше, хук useReducer следует использовать, когда логика вашего состояния немного сложнее или зависит от предыдущего состояния.

✅ Хорошие варианты использования useReducer:

  • Изменение 1 части состояния также изменяет другие (связанные значения состояния);
  • Состояние сложное и имеет много движущихся частей;
  • Когда вы хотите/нужны более предсказуемые переходы между состояниями;

Крюк useReducer

Теперь, когда у нас есть некоторый контекст о том, где использовать этот хук, пришло время поближе взглянуть на его API.

useReducer это встроенная функция React с двумя разными сигнатурами:

  • useReducer(reducer, initialArg);
  • useReducer(reducer, initialArg, init);

useReducer аргументов

reducer

reducer, как указывает само название, это функция, которая берет некоторую информацию и сводит ее во что-то, и именно здесь происходит «волшебство».

Он принимает два аргумента: текущий state и action, который отправляется пользовательским интерфейсом. Принимая заданный тип действия, редюсер возвращает следующую часть состояния, обычно производя предыдущее состояние.

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
  }
}

initialState

Этот аргумент говорит сам за себя, это просто состояние, с которого начинается хук useReducer.

init

init — это функция, которая позволяет вам выполнять некоторую логику вокруг начального состояния, поскольку она будет принимать значение, которое вы передали как initialState, и возвращать «новое» initialState на основе этого.

function init(initialCount) {
  return {count: initialCount};
}

useReducer возвращенных значений

Очень похожий на useState, этот хук возвращает массив с двумя значениями:

  • Первый, чтобы показать текущее состояние;
  • Второй способ изменить состояние и создать повторный рендер в приложении.
const [state, dispatch] = useReducer(counterReducer, initialState);

state

Это значение не требует особых пояснений, это просто текущее состояние, возвращаемое хуком useReducer.

dispatch

Это функция, в которой вы можете передать возможные actions, которые вы определяете для обработки reducer. Если взять в качестве примера предыдущий counterReducer, это может выглядеть так:

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

Управление логикой выборки с помощью хука useReducer

Теперь, когда мы лучше понимаем, что может сделать для нас хук useReducer, пришло время запачкать руки и использовать этот хук React для обработки любого заданного состояния, связанного с получением.

Получение состояния

Чтобы использовать useReducer, вы должны сначала подумать, каким будет состояние, которым вы хотите управлять, обычно это все, что у вас может быть в связке хуков useState, таких как data, errorMessage, fetchState и т. д.

В этом сценарии, поскольку мы хотим создать хук, который позволит нам управлять логикой выборки. Что касается логики выборки, все, что вам нужно, это:

  • состояние: узнать, является ли приложение iddle, loading, была ли выборка success или failure
  • ошибка: сообщение об ошибке на случай, если что-то пошло не так.
  • данные: данные ответа.

Итак, теперь, когда мы определили нашу структуру state, мы можем настроить нашу initialState.

// "iddle" state because we haven't fetch anything yet!
  const initialState = {
    status: "idle",
    data: null,
    error: null,
  };

Получение редуктора

Действия

Второй шаг — создать логику, которая приведет к различным состояниям приложения. Эта логика находится в функции reducer, и для того, чтобы смонтировать эту логику, мы должны начать с размышлений о «действиях», которые нам нужно выполнить.

Для логики выборки нам понадобятся следующие действия:

  • FETCH: действие, которое будет вызвано при запуске запроса;
  • RESOLVE: действие, которое будет вызвано в случае успешного ответа;
  • REJECT: действие, которое будет вызвано, если запрос выдает ошибку или ответ является «недействительным»;

Помните, что вы можете называть эти действия как хотите, если они отражают то, что делается, и это имеет смысл для вас.

Переходы состояний

Каждое из этих действий (FETCH, RESOLVE и REJECT) приведет к переходу состояния, таким образом создавая новый результат (новое состояние).

Итак, теперь нужно просто выяснить, какое состояние будет выведено каждым из этих действий.

FETCH

RESOLVE

REJECT

Реализация useReducer

Со всем псевдокодом и решениями, которые мы приняли выше, теперь мы можем использовать useReducer для управления логикой выборки:

const initialState = {
    status: "idle",
    data: null,
    error: null
  };
  function fetchReducer(currentState, action) {
    switch (action.type) {
      case "FETCH":
        return {
          ...currentState,
          status: "loading"
        };
      case "RESOLVE":
        return {
          status: "success",
          data: action.data,
          error: null
        };
      case "REJECT":
        return {
          data: null,
          status: "failure",
          error: action.error
        };
      default:
        return currentState;
    }
  }
  const [state, dispatch] = React.useReducer(fetchReducer, initialState);
}

Получение данных

Код реализации готов, теперь давайте проверим, как бы выглядел код, если бы мы получали некоторые данные через наш useReducer.

function fetchIt() {
    // Start fetching!
    dispatch({ type: "FETCH" });
    fetch("https://www.reddit.com/r/padel.json")
      .then((response) =>
        response.json().then((result) => {
          // We got our data!
            dispatch({ type: "RESOLVE", data: result });
        })
      )
      .catch((error) => {
       // We got an error!
        dispatch({ type: "REJECT", data: error });
      });
  }
return (
    <>
      {state.status === "loading" ? <p>loading...</p> : undefined}
      {state.status === "success" ? <p>{JSON.stringify(state.data)}</p> : undefined}
      {state.status === "failure" ? <p>{JSON.stringify(state.error)}</p> : undefined}
      <button disabled={state.status === "loading"} onClick={fetchIt}>
        Fetch Data
      </button>
    </>
  );

Создание пользовательского хука useFetchReducer

Теперь вы, вероятно, захотите использовать этот самый код для управления состоянием вашего приложения в каждом месте, где вы выполняете HTTP-запрос.

К счастью для нас, React обладает огромной силой композиции, упрощающей нашу жизнь при создании пользовательских хуков с помощью других существующих хуков React (в данном случае useReducer).

Извлечение крючка useReducer

Первый шаг — создать новый файл с именем use-fetch-reducer.js или как вы его назовете, главное, чтобы он начинался с use (чтобы его можно было идентифицировать как хук).

2-й шаг — взять (скопировать) весь код, который мы реализовали ранее, и вставить его в экспортированную функцию с именем useFetchReducer. Это должно выглядеть примерно так:

import React from "react";
export function useFetchReducer() {
  const initialState = {
    status: "idle",
    data: null,
    error: null
  };
  function fetchReducer(currentState, action) {
    switch (action.type) {
      case "FETCH":
        return {
          ...currentState,
          status: "loading"
        };
      case "RESOLVE":
        return {
          status: "success",
          data: action.data,
          error: null
        };
      case "REJECT":
        return {
          data: null,
          status: "failure",
          error: action.error
        };
      default:
        return currentState;
    }
  }
  const [state, dispatch] = React.useReducer(fetchReducer, initialState);
}

3-й шаг — извлечь наш результат useReducer и вернуть его, чтобы мы могли использовать state и dispatch во всех остальных компонентах:

//...
return React.useReducer(fetchReducer, initialState);

Подводя итог, мы должны сделать этот хук как можно более «универсальным», чтобы он мог удовлетворить потребности каждого компонента, из которого он вызывается. Чтобы достичь этого, 4-й шаг проходит, предоставляя потребителям возможность самостоятельно установить initialData, поскольку он не всегда может начинаться как null:

function useFetchReducer(initialData = null) {
  const initialState = {
    status: "idle",
    data: initialData,
    error: null
  };
//...

Окончательный код

Использование useFetchReducer

  1. Импортируйте только что созданный хук в свой компонент;
  2. Выполнить как const [state, dispatch] = useFetchReducer();
  3. Используйте его state и dispatch так же, как для крючка useReducer.

Рабочий код

гифка

Заключение

Если состояние вашего приложения становится несколько сложным, а количество useState растет, возможно, пришло время сделать небольшой переход и вместо этого воспользоваться преимуществами useReducer.

Если вы решили использовать useReducer, выполните следующие действия:

  1. Подумайте об состоянии, которым вы хотите управлять;
  2. Подумайте об Действиях, которые вызывают переходы между состояниями;
  3. Подумайте об переходах между состояниями, которые будут происходить при вызове определенного набора состояний.

После этих мыслей пришло время написать свой собственный редюсер и вызвать хук useReducer.

Если логику, которую вы только что создали, можно повторно использовать в вашем приложении, создайте собственный хук и наслаждайтесь 😉

Подпишитесь на меня в Твиттере, если вы хотите прочитать о лучших практиках TypeScript или просто о веб-разработке в целом!