Освоение 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
- Импортируйте только что созданный хук в свой компонент;
- Выполнить как
const [state, dispatch] = useFetchReducer();
- Используйте его
state
иdispatch
так же, как для крючкаuseReducer
.
гифка
Заключение
Если состояние вашего приложения становится несколько сложным, а количество useState
растет, возможно, пришло время сделать небольшой переход и вместо этого воспользоваться преимуществами useReducer
.
Если вы решили использовать useReducer
, выполните следующие действия:
- Подумайте об состоянии, которым вы хотите управлять;
- Подумайте об Действиях, которые вызывают переходы между состояниями;
- Подумайте об переходах между состояниями, которые будут происходить при вызове определенного набора состояний.
После этих мыслей пришло время написать свой собственный редюсер и вызвать хук useReducer
.
Если логику, которую вы только что создали, можно повторно использовать в вашем приложении, создайте собственный хук и наслаждайтесь 😉
Подпишитесь на меня в Твиттере, если вы хотите прочитать о лучших практиках TypeScript или просто о веб-разработке в целом!