Хотите заказать веб-сайт? Связаться с нами

Redux в React: работа с состоянием приложения

Redux — одна из самых известных библиотек для управления состоянием в React-приложениях. В этом уроке разберёмся, зачем нужен (откроется в новой вкладке)Redux, из каких частей он состоит и как правильно подключить его к проекту с помощью Redux Toolkit.

Redux в React: работа с состоянием приложения

Зачем нужен Redux в React-приложениях

React из коробки предлагает достаточно инструментов для управления состоянием — useState и useContext. Пока приложение небольшое, этого хватает с лихвой. Но как только проект растёт, использование только локального состояния приводит к проблеме — prop drilling: данные приходится прокидывать через десятки промежуточных компонентов, которые сами их не используют, а просто передают дальше.

Здесь на помощь приходит Redux — библиотека для предсказуемого управления состоянием приложения. Redux хранит всё состояние в едином объекте — сторе (store), к которому может обратиться любой компонент, независимо от того, насколько глубоко он находится в дереве компонентов. Это избавляет от необходимости пробрасывать пропсы через промежуточные уровни и делает поведение приложения более предсказуемым: изменения состояния происходят по строгим правилам, а не хаотично из разных мест кода.

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

Три принципа Redux

В основе Redux лежат три простых принципа, на которых строится вся библиотека.

  1. Единый источник истины. Всё состояние приложения хранится в одном объекте внутри store. Это упрощает отладку и позволяет легко сериализовать состояние, например, для сохранения истории действий.
  2. Состояние доступно только для чтения. Единственный способ изменить состояние — отправить action, описывающий, что произошло. Компоненты не могут напрямую менять данные в сторе.
  3. Изменения описываются чистыми функциями. За обработку действий отвечают 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 — ещё одной популярной библиотекой для управления состоянием, которая предлагает иной, более объектно-ориентированный подход к работе с данными.

Теги: