Redux в React: работа с состоянием приложения
Redux — одна из самых известных библиотек для управления состоянием в React-приложениях. В этом уроке разберёмся, зачем нужен (откроется в новой вкладке)Redux, из каких частей он состоит и как правильно подключить его к проекту с помощью Redux Toolkit.
Зачем нужен Redux в React-приложениях
useState и useContext. Пока приложение небольшое, этого хватает с лихвой. Но как только проект растёт, использование только локального состояния приводит к проблеме — prop drilling: данные приходится прокидывать через десятки промежуточных компонентов, которые сами их не используют, а просто передают дальше.Здесь на помощь приходит Redux — библиотека для предсказуемого управления состоянием приложения. Redux хранит всё состояние в едином объекте — сторе (store), к которому может обратиться любой компонент, независимо от того, насколько глубоко он находится в дереве компонентов. Это избавляет от необходимости пробрасывать пропсы через промежуточные уровни и делает поведение приложения более предсказуемым: изменения состояния происходят по строгим правилам, а не хаотично из разных мест кода.
Context API решает похожую задачу, но хуже справляется с частыми обновлениями: любое изменение значения контекста перерисовывает все компоненты, которые на него подписаны. Redux же позволяет каждому компоненту подписываться только на нужную часть состояния, поэтому в крупных приложениях с частыми изменениями данных он показывает себя лучше.
Три принципа Redux
В основе Redux лежат три простых принципа, на которых строится вся библиотека.
- Единый источник истины. Всё состояние приложения хранится в одном объекте внутри
store. Это упрощает отладку и позволяет легко сериализовать состояние, например, для сохранения истории действий. - Состояние доступно только для чтения. Единственный способ изменить состояние — отправить
action, описывающий, что произошло. Компоненты не могут напрямую менять данные в сторе. - Изменения описываются чистыми функциями. За обработку действий отвечают
reducer-функции — они принимают текущее состояние и action, а возвращают новое состояние, не изменяя исходные данные.
Эти принципы делают поведение приложения предсказуемым: зная начальное состояние и список выполненных действий, всегда можно точно сказать, каким будет итоговое состояние.
Store, actions и reducers — основные части Redux
Чтобы Redux заработал в проекте, нужно разобраться с тремя ключевыми сущностями.
Store — это объект, который хранит всё состояние приложения и предоставляет методы для чтения состояния, отправки действий и подписки на изменения.
Action — обычный JavaScript-объект с обязательным полем type, которое описывает, какое событие произошло. Дополнительные данные передаются в поле payload.
Reducer — чистая функция, которая принимает текущее состояние и action, а возвращает новое состояние. Reducer никогда не должен мутировать исходные данные — вместо этого он создаёт новый объект состояния.
Вот как выглядит простой reducer для счётчика:
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
Каждый раз, когда в store отправляется action, Redux вызывает reducer, передаёт ему текущее состояние и action, а затем сохраняет то, что вернул reducer, как новое состояние. Если в приложении несколько независимых частей состояния, их reducers обычно объединяют функцией combineReducers, чтобы каждый из них отвечал только за свой участок общего объекта.
Не храните в Redux данные, которые нужны только одному компоненту — например, состояние открытого или закрытого модального окна на одной странице. Такие вещи лучше оставить в локальном состоянии через useState, а в глобальный store выносите только то, что реально нужно нескольким частям приложения.
Redux Toolkit — современный способ работы с Redux
Классический Redux требует много шаблонного кода: отдельно нужно описывать типы действий, action-креаторы и reducers, а также следить, чтобы состояние нигде не мутировалось напрямую. Чтобы упростить эту работу, команда Redux выпустила официальный набор инструментов — (откроется в новой вкладке)Redux Toolkit, сокращённо RTK.
Главный инструмент RTK — функция createSlice. Она объединяет reducer, action-креаторы и типы действий в одном месте, а под капотом использует библиотеку Immer, которая позволяет писать код так, будто вы напрямую изменяете состояние, хотя на самом деле создаётся новый неизменяемый объект.
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { count: 0 },
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
Функция configureStore заменяет ручную настройку store и уже включает в себя полезные middleware, например для проверки на случайные мутации состояния и подключения Redux DevTools. Это расширение браузера показывает историю всех отправленных действий и позволяет откатывать состояние назад, что сильно упрощает отладку сложной логики.
| Классический Redux | Redux Toolkit |
|---|---|
| Ручное создание типов действий | Типы создаются автоматически внутри createSlice |
| Reducer нельзя мутировать состояние напрямую | Можно писать «мутирующий» код благодаря Immer |
Store настраивается вручную через createStore |
Store создаётся через configureStore с настройками по умолчанию |
Подключение Redux к React через react-redux
Чтобы React-компоненты могли читать данные из Redux store и отправлять действия, используется библиотека (откроется в новой вкладке)React Redux. Она предоставляет компонент Provider и набор хуков для работы с store.
Компонент Provider оборачивает всё приложение и передаёт store через React Context, поэтому доступ к нему получают все вложенные компоненты, независимо от глубины вложенности.
Для чтения данных из store применяется хук useSelector, а для отправки действий — useDispatch. Раньше для этого использовалась функция connect, оборачивающая компонент и передающая ему данные из store через пропсы, но в современных проектах хуки удобнее и требуют меньше кода.
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./counterSlice";
function Counter() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
}
Компонент подписывается на нужную часть состояния через useSelector и автоматически перерисовывается, когда эта часть состояния меняется.
Старайтесь делать селекторы в useSelector максимально узкими — выбирайте только те данные, которые реально нужны компоненту. Если селектор вычисляет производные данные, например отфильтрованный список, оберните его в createSelector из библиотеки reselect, чтобы не пересчитывать результат при каждом рендере.
Итоги
В этом уроке мы разобрались, зачем нужен Redux, изучили три его принципа и рассмотрели основные части — store, actions и reducers. Также посмотрели, как Redux Toolkit упрощает написание кода, и как с помощью react-redux подключить store к React-компонентам через Provider, useSelector и useDispatch.
В следующем уроке познакомимся с MobX — ещё одной популярной библиотекой для управления состоянием, которая предлагает иной, более объектно-ориентированный подход к работе с данными.