Подключение useReducer в React
useReducer один из дополнительных хуков, поставляемых с React v16.8. Альтернатива хуку useState, useReducerпомогает управлять сложной логикой состояний в приложениях React. В сочетании с другими хуками, такими как useContext, useReducerможет быть хорошей альтернативой Redux, Recoil или MobX . В некоторых случаях это просто лучший вариант.
Хотя Redux, Recoil и MobX обычно являются лучшими вариантами управления глобальными состояниями в крупных приложениях React, чаще, чем это необходимо, многие разработчики React обращаются к этим сторонним библиотекам управления состоянием, когда они могли бы эффективно управлять своим состоянием с помощью перехватчиков.
В этом руководстве мы подробно рассмотрим механизм useReducer, рассмотрев сценарии, в которых вам следует и не следует его использовать. Давайте начнем!
Как работает Hook useReducer?
Перехватчик useReducer используется для хранения и обновления состояний, так же как и перехватчик useState. В качестве первого параметра он принимает функцию восстановления, а в качестве второго - начальное состояние. useReducer возвращает массив, содержащий текущее значение состояния, и функцию отправки, в которую вы можете передать действие и позже вызвать его. Хотя это похоже на шаблон, который использует Redux, есть несколько отличий.
Например, функция useReducer тесно связана с определенным редуктором. Мы отправляем объекты action только в этот редуктор, тогда как в Redux функция dispatch отправляет объект action в хранилище. Во время отправки компонентам не нужно знать, какой редуктор будет обрабатывать действие.
Для тех, кто, возможно, не знаком с Redux, мы рассмотрим эту концепцию немного подробнее. В Redux есть три основных компонента:
- Хранилище: неизменяемый объект, содержащий данные о состоянии приложения.
- Редуктор: функция, которая возвращает некоторые данные о состоянии, запускаемые типом действия.
- Действие: объект, который сообщает редуктору, как изменить состояние. Он должен содержать свойство type и может содержать необязательное свойство полезной нагрузки
Давайте посмотрим, как эти базовые элементы соотносятся с управлением состоянием с помощью функции useReducer. Ниже приведен пример хранилища в Redux:
import { createStore } from 'redux'
const store = createStore(reducer, [preloadedState], [enhancer])
В приведенном ниже коде мы инициализируем состояние с помощью перехватчика useReducer:
const initialState = { count: 0 }
const [state, dispatch] = useReducer(reducer, initialState)
Функция редуктора в Redux принимает предыдущее состояние приложения и отправляемое действие, вычисляет следующее состояние и возвращает новый объект. Синтаксис редукторов в Redux приведен ниже:
(state = initialState, action) => newState
Давайте рассмотрим следующий пример:
// notice that the state = initialState and returns a new state
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ITEMS_REQUEST':
return Object.assign({}, state, {
isLoading: action.payload.isLoading
})
case ‘ITEMS_REQUEST_SUCCESS':
return Object.assign({}, state, {
items: state.items.concat(action.items),
isLoading: action.isLoading
})
default:
return state;
}
}
export default reducer;
useReducer не использует шаблон (state = initialState, action) => newState Redux, поэтому его функция редуктора работает немного по-другому. В приведенном ниже коде показано, как вы могли бы создавать редукторы с помощью useReducer от React:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
Ниже приведен пример действия, которое может быть выполнено в Redux:
{ type: ITEMS_REQUEST_SUCCESS, payload: { isLoading: false } }
// action creators
export function itemsRequestSuccess(bool) {
return {
type: ITEMS_REQUEST_SUCCESS,
payload: {
isLoading: bool,
}
}
}
// dispatching an action with Redux
dispatch(itemsRequestSuccess(false)) // to invoke a dispatch function, you need to pass action as an argument to the dispatch function
Действия в useReducer работают аналогичным образом:
// not the complete code
switch (action.type) {
case 'increment':
return {count: state.count + 1};
default:
throw new Error();
}
// dispatching an action with useReducer
<button onClick={() => dispatch({type: 'increment'})}>Increment</button>
Если тип действия в приведенном выше коде - increment, то наш объект состояния увеличивается на 1.
Функция редуктора
Метод JavaScript reduce() выполняет функцию-редуктор для каждого элемента массива и возвращает одно значение. Метод reduce() принимает функцию-редуктор, которая сама может принимать до четырех аргументов. Приведенный ниже фрагмент кода иллюстрирует, как работает редуктор:
const reducer = (accumulator, currentValue) => accumulator + currentValue;
[2, 4, 6, 8].reduce(reducer)
// expected output: 20
В React useReducer, по сути, принимает функцию-редуктор, которая возвращает одно значение:
const [count, dispatch] = useReducer(reducer, initialState);
Сама функция reducer принимает два параметра и возвращает одно значение. Первый параметр - это текущее состояние, а второй - действие. Состояние - это данные, которыми мы манипулируем. Функция reducer получает действие, которое выполняется диспетчерской функцией:
function reducer(state, action) { }
dispatch({ type: 'increment' })
Действие похоже на инструкцию, которую вы передаете функции reducer. На основе указанного действия функция reducer выполняет необходимое обновление состояния. Если вы раньше использовали библиотеку управления состоянием, такую как Redux, то, вероятно, сталкивались с этим шаблоном управления состоянием.
Указание начального состояния
Начальное состояние - это второй аргумент, передаваемый перехватчику useReducer, который представляет состояние по умолчанию:
const initialState = { count: 1 }
// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialState, initFunc)
Если вы не передадите третий аргумент в useReducer, он примет второй аргумент в качестве начального состояния. Третий аргумент, который является функцией init, необязателен. Этот шаблон также соответствует одному из золотых правил управления состоянием Redux: состояние должно обновляться путем выполнения действий. Никогда не записывайте данные непосредственно в состояние.
Однако стоит отметить, что соглашение Redux state = initialState не работает таким же образом с useReducer, поскольку начальное значение иногда зависит от props.
Лениво создаем исходное состояние
В программировании ленивая инициализация - это тактика откладывания создания объекта, вычисления значения или какого-либо другого дорогостоящего процесса до тех пор, пока это не понадобится в первый раз.
Как упоминалось выше, useReducer может принимать третий параметр, который является необязательной функцией инициализации для ленивого создания начального состояния. Это позволяет извлекать логику для вычисления начального состояния вне функции reducer, как показано ниже:
const initFunc = (initialCount) => {
if (initialCount !== 0) {
initialCount=+0
}
return {count: initialCount};
}
// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialCount, initFunc);
Если значение еще не равно 0, описанная выше функция initFunc сбросит значение initialCount до 0 при монтировании страницы, а затем вернет объект state. Обратите внимание, что эта функция initFunc является функцией, а не просто массивом или объектом.
Что такое диспетчеризация в React
Функция dispatch принимает объект, который представляет тип действия, которое мы хотим выполнить при его вызове. По сути, она отправляет тип действия функции reducer для выполнения ее задания, которое, конечно же, заключается в обновлении состояния. Думайте о диспетчере как о посыльном, который доставляет инструкции (действия) государственному менеджеру (редуктору).
Действие, которое необходимо выполнить, указывается в нашей функции-редукторе, которая, в свою очередь, передается в useReducer. Затем функция-редуктор вернет обновленное состояние.
Действия, которые будут отправляться нашими компонентами, всегда должны быть представлены в виде одного объекта с ключом type и полезной нагрузкой, где type означает идентификатор отправляемого действия, а полезная нагрузка - это часть информации, которую это действие добавит к состоянию. dispatch - это второе значение, возвращаемое перехватчиком useReducer, и его можно использовать в нашем JSX для обновления состояния:
// creating our reducer function
function reducer(state, action) {
switch (action.type) {
// ...case 'reset':
return { count: action.payload };
default:
throw new Error();
}
}
// wherever our useReducer is located
const [state, dispatch] = useReducer(reducer, initialCount, initFunc);
// Updating the state with the dispatch functon on button click
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button>
Обратите внимание, как наша функция reducer использует полезную нагрузку, передаваемую из функции dispatch. Она присваивает нашему объекту state значение полезной нагрузки, т.е. независимо от значения initialCount. Обратите внимание, что мы можем передать функцию отправки другим компонентам через props, что само по себе позволяет нам заменить Redux на useReducer.
Допустим, у нас есть компонент, который мы хотим передать в качестве props нашей функции отправки. Мы можем легко сделать это из родительского компонента:
<Increment count={state.count} handleIncrement={() => dispatch({type: 'increment'})}/>
Теперь в дочернем компоненте мы получаем props, который при отправке запускает функцию отправки и обновляет состояние:
<button onClick={handleIncrement}>Increment</button>
Спасаясь от отправки
Если функция useReducer возвращает то же значение, что и текущее состояние, React завершит работу без рендеринга дочерних элементов или запуска эффектов, поскольку использует алгоритм сравнения Object.is.
Создание простого приложения-счетчика с помощью useReducer Hook
Теперь давайте применим наши знания на практике, создав простое приложение-счетчик с помощью хука useReducer:
import React, { useReducer } from 'react';
const initialState = { count: 0 }
// The reducer function
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return {count: state.count = 0}
default:
return { count: state.count }
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
Count: {state.count}
<br /><br/><button onClick={() => dispatch({ type: 'increment' })}>Increment</button><button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button><button onClick={() => dispatch({ type: 'reset'})}>Reset</button></div>
);
};
export default Counter;
Сначала мы инициализируем состояние значением 0, затем создаем функцию-редуктор, которая принимает текущее состояние нашего счетчика в качестве аргумента и действия. Состояние обновляется редуктором в зависимости от типа действия. увеличение, уменьшение и сброс - это все типы действий, которые при отправке соответствующим образом обновляют состояние нашего приложения.
Чтобы увеличить значение state count const initialState = { count: 0 }, мы просто устанавливаем значение count равным state.count + 1 при отправке типа действия increment.
useState против useReducer
useState - это базовый инструмент для управления простым преобразованием состояний, а useReducer - дополнительный инструмент для управления более сложной логикой состояний. Однако стоит отметить, что useState использует useReducer внутренне, подразумевая, что вы можете использовать useReducer для всего, что вы можете делать с useState.
Однако между этими двумя перехватчиками есть некоторые существенные различия. С помощью useReducer вы можете избежать передачи обратных вызовов на разных уровнях вашего компонента. Вместо этого useReducer позволяет передавать предоставленную функцию диспетчеризации, что, в свою очередь, повысит производительность компонентов, запускающих глубокие обновления.
Однако это не означает, что функция useState updater вызывается заново при каждом рендеринге. Если у вас сложная логика обновления состояния, вы просто не будете использовать средство настройки напрямую для обновления состояния. Вместо этого вы напишете сложную функцию, которая, в свою очередь, вызовет установщик с обновленным состоянием.
Поэтому рекомендуется использовать useReducer, который возвращает метод диспетчеризации, который не меняется между повторными отображениями, и позволяет вам использовать логику манипулирования в редукторах.
Также стоит отметить, что при использовании useState для обновления состояния вызывается функция state updater, но при использовании useReducer вместо этого вызывается функция dispatch, и ей передается действие, имеющее как минимум один тип. Теперь давайте посмотрим, как объявляются и используются оба перехватчика.
Объявление состояния с помощью useState
useState возвращает массив, содержащий текущее значение состояния и метод setState для обновления состояния:
const [state, setState] = useState('default state');
Объявление состояния с помощью useReducer
useReducer возвращает массив, содержащий текущее значение состояния, и метод диспетчеризации, который логически достигает той же цели, что и setState, обновляя состояние:
const [state, dispatch] = useReducer(reducer, initialState)
Обновление состояния с помощью useState происходит следующим образом:
<input type='text' value={state} onChange={(e) => setState(e.currentTarget.value)} />
Обновление состояния с помощью useReducer происходит следующим образом:
<button onClick={() => dispatch({ type: 'decrement'})}>Decrement</button>
Мы более подробно обсудим функцию диспетчеризации позже в руководстве. Необязательно, объект действия также может содержать полезную нагрузку:
<button onClick={() => dispatch({ type: 'decrement', payload: 0})}>Decrement</button>
useReducer может быть полезен при управлении сложными формами состояний, например, когда состояние состоит из более чем примитивных значений, таких как вложенные массивы или объекты:
const [state, dispatch] = useReducer(loginReducer,
{
users: [
{ username: 'Philip', isOnline: false},
{ username: 'Mark', isOnline: false },
{ username: 'Tope', isOnline: true},
{ username: 'Anita', isOnline: false },
],
loading: false,
error: false,
},
);
Управлять этим локальным состоянием проще, потому что параметры зависят друг от друга, и вся логика может быть заключена в один редуктор.
Когда следует использовать useReducer Hook
По мере увеличения размера вашего приложения вы, скорее всего, будете иметь дело с более сложными переходами состояний, и в этот момент вам будет лучше использовать useReducer. useReducer обеспечивает более предсказуемые переходы состояний, чем useState, что становится более важным, когда изменения состояния становятся настолько сложными, что требуется иметь одно место для управления состоянием, например, функцию рендеринга.
useReducer - лучший вариант, когда вы переходите от управления примитивными данными, т.е. строкой, целым числом или логическим значением, и вместо этого должны управлять сложным объектом, например, массивами и дополнительными примитивами.
Создание компонента входа в систему
Чтобы лучше понять, когда следует использовать useReducer, давайте создадим компонент входа в систему и сравним, как бы мы управляли состоянием с помощью перехватчиков useState и useReducer.
Во-первых, давайте создадим компонент входа в систему с помощью useState:
import React, { useState } from 'react';
export default function LoginUseState() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, showLoader] = useState(false);
const [error, setError] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const onSubmit = async (e) => {
e.preventDefault();
setError('');
showLoader(true);
try {
await new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'ejiro' && password === 'password') {
resolve();
} else {
reject();
}
}, 1000);
});
setIsLoggedIn(true);
} catch (error) {
setError('Incorrect username or password!');
showLoader(false);
setUsername('');
setPassword('');
}
};
return (
<div className='App'><div className='login-container'>
{isLoggedIn ? (
<><h1>Welcome {username}!</h1><button onClick={() => setIsLoggedIn(false)}>Log Out</button></>
) : (
<form className='form' onSubmit={onSubmit}>
{error && <p className='error'>{error}</p>}
<p>Please Login!</p><inputtype='text'placeholder='username'value={username}onChange={(e) => setUsername(e.currentTarget.value)}
/>
<inputtype='password'placeholder='password'autoComplete='new-password'value={password}onChange={(e) => setPassword(e.currentTarget.value)}
/>
<button className='submit' type='submit' disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Log In'}
</button></form>
)}
</div>
</div>
);
}
Обратите внимание, как мы справляемся со всеми этими переходами состояний, такими как username, password, isLoading, error и isLoggedIn, когда на самом деле нам следует больше сосредоточиться на действии, которое пользователь хочет выполнить в компоненте входа в систему.
Мы использовали пять перехватчиков useState, и нам пришлось беспокоиться о том, когда происходит переход в каждое из этих состояний. Мы можем реорганизовать приведенный выше код, чтобы использовать useReducer и инкапсулировать всю нашу логику и переходы состояний в одну функцию-редуктор:
import React, { useReducer } from 'react';
function loginReducer(state, action) {
switch (action.type) {
case 'field': {
return {
...state,
[action.fieldName]: action.payload,
};
}
case 'login': {
return {
...state,
error: '',
isLoading: true,
};
}
case 'success': {
return {
...state,
isLoggedIn: true,
isLoading: false,
};
}
case 'error': {
return {
...state,
error: 'Incorrect username or password!',
isLoggedIn: false,
isLoading: false,
username: '',
password: '',
};
}
case 'logOut': {
return {
...state,
isLoggedIn: false,
};
}
default:
return state;
}
}
const initialState = {
username: '',
password: '',
isLoading: false,
error: '',
isLoggedIn: false,
};
export default function LoginUseReducer() {
const [state, dispatch] = useReducer(loginReducer, initialState);
const { username, password, isLoading, error, isLoggedIn } = state;
const onSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'login' });
try {
await new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'ejiro' && password === 'password') {
resolve();
} else {
reject();
}
}, 1000);
});
dispatch({ type: 'success' });
} catch (error) {
dispatch({ type: 'error' });
}
};
return (
<div className='App'><div className='login-container'>
{isLoggedIn ? (
<><h1>Welcome {username}!</h1><button onClick={() => dispatch({ type: 'logOut' })}>
Log Out
</button></>
) : (
<form className='form' onSubmit={onSubmit}>
{error && <p className='error'>{error}</p>}
<p>Please Login!</p><inputtype='text'placeholder='username'value={username}onChange={(e) =>
dispatch({
type: 'field',
fieldName: 'username',
payload: e.currentTarget.value,
})
}
/>
<inputtype='password'placeholder='password'autoComplete='new-password'value={password}onChange={(e) =>
dispatch({
type: 'field',
fieldName: 'password',
payload: e.currentTarget.value,
})
}
/>
<button className='submit' type='submit' disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Log In'}
</button></form>
)}
</div>
</div>
);
}
Обратите внимание, что новая реализация useReducer позволила нам больше сосредоточиться на действии, которое собирается предпринять пользователь. Например, когда действие входа в систему отправлено, мы можем четко видеть, что мы хотим, чтобы произошло. Мы хотим вернуть копию нашего текущего состояния, присвоить нашей ошибке значение пустой строки, а isLoading - значение true:
case 'login': {
return {
...state,
error: '',
isLoading: true,
};
}
Самое замечательное в нашей текущей реализации заключается в том, что нам больше не нужно фокусироваться на изменении состояния. Вместо этого мы уделяем особое внимание действиям, которые должен выполнять пользователь.
Давайте расширим существующий пример, чтобы показать, как обрабатывать вызовы API с помощью useReducer, включив фактический вызов API и обновив таблицу с данными ответа:
import React, { useReducer, useEffect } from 'react';
import axios from 'axios';
const initialState = {
users: [],
loading: false,
error: null
};
function userReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, users: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
function UserList() {
const [state, dispatch] = useReducer(userReducer, initialState);
useEffect(() => {
const fetchUsers = async () => {
dispatch({ type: 'FETCH_START' });
try {
const response = await axios.get('https://api.example.com/users');
dispatch({ type: 'FETCH_SUCCESS', payload: response.data });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
fetchUsers();
}, []);
const { users, loading, error } = state;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<table><thead><tr><th>ID</th><th>Name</th><th>Email</th></tr></thead><tbody>
{users.map(user => (
<tr key={user.id}><td>{user.id}</td><td>{user.name}</td><td>{user.email}</td></tr>
))}
</tbody></table>
);
}
В приведенном выше примере показано, как использовать useReducer для управления состоянием вызова API, включая его загрузку и состояние ошибки, и как обновлять таблицу с полученными данными.
Помните, что выбор между useState и useReducer часто зависит от сложности вашей логики состояния и личных предпочтений. Не бойтесь начинать с useState и перестраивать его на useReducer, если вы обнаружите, что ваша логика состояния становится слишком сложной.
Когда не следует использовать useReducer Hook
Несмотря на возможность использования useReducer для обработки сложной логики состояния в нашем приложении, важно отметить, что в некоторых сценариях сторонняя библиотека управления состоянием, такая как Redux, может быть лучшим вариантом:
- Когда вашему приложению нужен единый источник достоверной информации
- Когда вы хотите получить более предсказуемое состояние
- Когда изменения состояния компонента верхнего уровня больше не достаточно
- Когда вам нужно сохранить состояние даже после обновления страницы
При всех этих преимуществах также стоит отметить, что использование такой библиотеки, как Redux, в отличие от использования чистого React с useReducer, сопряжено с некоторыми компромиссами. Например, Redux требует значительного времени на изучение, которое сводится к минимуму при использовании Redux Toolkit, и это определенно не самый быстрый способ написания кода. Скорее, он предназначен для того, чтобы предоставить вам абсолютный и предсказуемый способ управления состоянием вашего приложения.
Устранение распространенных неполадок с помощью useReducer
Ниже приведены наиболее распространенные проблемы, с которыми вы можете столкнуться при использовании useReducer. В основном они вызваны ошибками разработчика, а не проблемой с перехватом:
- Несогласованное действие: это может быть вызвано отправкой неверных или несогласованных типов или полезной нагрузки для одного и того же действия, что приводит к ошибкам. Чтобы решить эту проблему и обеспечить согласованность всех действий во всем приложении, вы можете попробовать использовать концепцию, подобную Redux Action Creators
- Неправильное обновление состояния: Еще одна распространенная ошибка, которая случается, заключается в том, как обновляется состояние в функции reducer. Это происходит, когда начальное состояние обрабатывается непосредственно в функции: state.count += 1; возвращает состояние; Это может привести к ошибкам. Решением было бы использовать операцию распространения или другие неизменяемые методы, чтобы не влиять на исходное состояние: return {...state, count: state.count + 1 }
- Чрезмерно сложная функция: Другая проблема заключается в том, что состояние может стать слишком сложным, что затруднит управление. В этом случае рассмотрите возможность разделения его на отдельные функции для упрощения
useReducer в React 19
Хотя useReducer не получил никаких обновлений в новой версии React 19, важно понимать его место в более широкой экосистеме React.
Серверные компоненты и пользовательский преобразователь
С появлением серверных компонентов React в React 19 стоит отметить, что useReducer (как и все перехватчики) может использоваться только в клиентских компонентах. Серверные компоненты не имеют состояния и не могут использовать перехватчики.
useReducer и использовать()
В React 19 представлен новый хук use() , который можно использовать для использования promises или контекста. Хотя он напрямую не связан с useReducer, хук use() может дополнять useReducer при работе с асинхронными данными в ваших редукторах.
import { use, useReducer } from 'react';
// ...
const [state, dispatch] = useReducer(reducer, initialState);
function fetchUser() {
dispatch({ type: 'FETCH_USER_START' });
try {
const user = use(fetchUserData(userId));
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_ERROR', payload: error.message });
}
}
// ...
useReducer и useTransition перехватывают
С введением useTransition в React 18 и расширением его возможностей в React 19 мы можем объединить его с useReducer для создания более лаконичной логики, особенно при работе с мутациями данных и асинхронными операциями.
useTransition позволяет нам помечать обновления как переходы, что говорит React о том, что их можно прерывать и не нужно блокировать пользовательский интерфейс. Это особенно полезно в сочетании с useReducer для обработки сложных обновлений состояния, которые могут включать вызовы API или другие операции, отнимающие много времени.
Вывод
В этой статье мы рассмотрели интерфейс useReducer в React, рассмотрели, как он работает, когда его использовать и сравнили с интерфейсом useState.
Помните, что цель состоит не в том, чтобы использовать useReducer везде, а в том, чтобы использовать его там, где это делает ваш код более понятным и управляемым. Как и в случае со всеми шаблонами в разработке программного обеспечения, важно понимать компромиссы и выбирать подходящий инструмент для работы.
Я надеюсь, вам понравилась эта статья, и обязательно оставьте комментарий, если у вас возникнут какие-либо вопросы. Удачного написания кода!