Скидка до 60% и курс по ИИ в подарок 3 дня 09 :37 :59 Выбрать курс
Код
#статьи

Redux: что это такое и зачем она нужна

Управляем состоянием приложения одной библиотекой.

Иллюстрация: Оля Ежак для Skillbox Media

Redux — это библиотека для управления состоянием данных и пользовательским интерфейсом в JavaScript-приложениях. Её используют, когда данных становится много и они меняются в разных компонентах ПО: это потенциально может привести к проблемам, например к использованию в функциях устаревших значений переменных.

Из этой статьи вы узнаете, как работать с Redux и из каких компонентов состоит библиотека. Также мы напишем простое приложение, отработав теорию на практике. Статья будет полезна в первую очередь начинающим фронтендерам, работающим с React.

Содержание


Для чего нужна Redux

Redux используют в приложениях с большим объёмом данных, которые часто обновляются и используются в разных частях интерфейса. В такой ситуации сложно поддерживать согласованность состояния программы вручную: отдельные функции могут обращаться к устаревшим значениям, а интерфейс — вести себя непредсказуемо.

Ключевой термин в абзаце выше, который важно знать, чтобы оценить преимущества Redux, — это состояние. Под ним понимают единый объект данных, который описывает текущее состояние всего приложения в конкретный момент времени. Можно сказать, что это «снимок» того, что сейчас знает о себе программа: значения переменных, активные элементы интерфейса, пользовательский ввод и так далее.

Redux решает проблему поддержания согласованности состояния приложения за счёт строгого набора правил. Библиотека однозначно указывает, кто может инициировать изменение данных, когда это допустимо и по каким правилам они обновляются. Ключевой принцип в том, что любой компонент в приложении не имеет права сам менять данные. Он может только запросить изменение, отправив об этом сообщение.

Такой подход основан на Flux-архитектуре. Чтобы разобраться в ней, сначала посмотрим на то, как происходит обмен данными в классической MVC-архитектуре.

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

MVC-архитектура: классический подход в разработке
Инфографика: Майя Мальгина для Skillbox Media

Для решения этой проблемы появилась Flux-архитектура с предсказуемым однонаправленным потоком данных. В ней изменения всегда идут по цепочке — от компонента (Action) через посредника (Dispatcher), который обновляет хранилище состояния приложения (Store) и, как результат, интерфейс (View). В отличие от MVC-архитектуры, интерфейс не может изменить состояние напрямую.

Каждое хранилище отвечает за свою часть данных, а состояние приложения распределено между ними и обновляется по единым правилам.

Flux-архитектура
Инфографика: Майя Мальгина для Skillbox Media

Redux — одна из практических реализаций Flux-архитектуры с важным отличием: на смену нескольким хранилищам (Store) приходит одно.

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

Проблему такой «бюрократии» частично решает Redux Toolkit (RTK) — набор инструментов от создателей библиотеки. RTK автоматизирует рутинные части процесса и позволяет разработчикам обойтись меньшим объёмом кода. О нём мы подробно поговорим в практической части статьи.

Из чего состоит Redux

Перейдём от концепции к реализации. Redux не вводит новые абстракции, а только задаёт строгие правила взаимодействия обычных сущностей JavaScript. В библиотеке есть шесть ключевых элементов: store, state, action, reducer, dispatch и subscribe.

В примерах кода в этом разделе мы будем использовать приложение интернет-провайдера с одним абонентом. Это поможет лучше понять компоненты библиотеки.

Store, dispatch и subscribe

Store — главный объект, который связывает все части Redux воедино. Он хранит состояние, предоставляет к нему доступ, позволяет его изменять и подписываться на изменения тем компонентам, которых эти изменения затрагивают.

В коде это JavaScript-объект, который создаётся функцией createStore() при запуске приложения и остаётся в единственном экземпляре во время его работы.

const store = createStore (reducer);

createStore() принимает в качестве аргумента reducer — функцию, которая содержит конкретные операции. Необязательным вторым параметром является изначальное состояние хранилища — массив значений для компонентов. Например, такой:

const store = createStore(reducer);

У store есть два важных метода — dispatch и subscribe. dispatch() — это метод хранилища, который принимает в качестве аргумента действие и передаёт его редьюсеру.

store.dispatch(action)

subscribe() — это метод, который передаёт компоненту новое состояние приложения. Без него фактические изменения не отображались бы в интерфейсе.

Метод принимает функцию-слушателя listener, которая будет автоматически запускаться каждый раз, когда кто-то вызывает dispatch:

store.subscribe(listener)

State

State — это снимок состояния приложения в конкретный момент времени. Он хранит данные, необходимые для отслеживания текущего статуса, — число пользователей, суммарную стоимость товаров в корзине, баланс счёта и другие изменяющиеся со временем данные.

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

const state = {
  user: { name: "Alex", id: 1 },
  balance = 0,
  isInternetActive: false
};

State в Redux доступен только для чтения. Вы не сможете написать state.balance = 500. Единственный способ изменить его — создать новый стейт через цепочку действий.

Action

Action описывает событие и то, как требуется изменить состояние в связи с ним. В коде это простой объект с двумя свойствами — обязательным type и необязательным payload:

const action = {                  // Действие с двумя свойствами 
  type: "PAY_INTERNET", // Что случилось? Оплата интернета
  payload: 500                   // Данные: сумма списания
};

const action = {                // Действие с одним свойством
  type: "RECONNECTION" // Что случилось? Переподключение
};

Reducer

Редьюсер определяет, как действие должно изменить состояние, и меняет его. В коде он представлен функцией, которая в качестве аргументов принимает текущее состояние и действие, а возвращает новое состояние. Так как редьюсер работает с разными действиями, то тело функции состоит из switch-case-конструкции.

Если редьюсер не знает, как обработать поступившее действие, то обязан вернуть текущее состояние как есть. Иначе приложение перестанет работать.

// Начальное состояние (баланс на момент открытия счёта)
const initialState = {
  balance: 1000,
  isInternetActive: false
};

const bankReducer = (state = initialState, action) => {
  switch (action.type) {

    case "PAY_INTERNET":
      return {
        ...state, // Копируем все поля старого стейта
        balance: state.balance - action.payload, // Обновляем баланс
        isInternetActive: true // Включаем интернет
      };

    case "DEPOSIT_MONEY":
      return {
        ...state,
        balance: state.balance + action.payload
      };

    // Если тип экшена нам незнаком, возвращаем текущий стейт без изменений 
    default:
      return state;
  }
};

Жизненный цикл одной операции

Соберём всё вместе. Как будет происходить оплата интернета в приложении, которое мы описали:

  • Пользователь нажимает кнопку «Оплатить».
  • Генератор действий создаёт объект: { type: «PAY_INTERNET», payload: 500 }.
  • Компонент отправляет этот объект в хранилище: store.dispatch (action).
  • Хранилище «просыпается» и передаёт текущий стейт и экшен редьюсеру.
  • Функция-редьюсер считает: 1000 − 500 = 500. Она возвращает новый объект стейта, где баланс равен 500, а интернет включён.
  • Хранилище сохраняет этот новый стейт вместо старого.
  • Интерфейс, подписанный на хранилище, видит обновление и перерисовывает баланс на экране.

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

Устанавливаем Redux и пишем приложение

Теория — это хорошо, но Redux проще понять на практике. В этом разделе мы напишем часть простого банковского приложения на React. В ней реализуем весь цикл: от создания хранилища до отрисовки кнопок «Пополнить» и «Снять» в интерфейсе.

Мы предполагаем, что React.js и Node.js у вас уже установлены. Если нет, то сначала установите Node.js, а уже с её помощью установите React.js.

Устанавливаем Redux

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

Redux Toolkit — это официальный рекомендуемый набор инструментов для эффективной работы с Redux. Он был создан, чтобы упростить работу с библиотекой.

Для старта работы с ним установите пакет @reduxjs/toolkit — он уже включает в себя ядро Redux, react-redux — библиотеку-связку, и Redux Toolkit. Для этого введите в терминале:

npm install @reduxjs/toolkit react-redux

Пишем приложение

Разделим процесс на четыре шага, чтобы не запутаться: пропишем логику, подключим Redux, нарисуем интерфейс и протестируем результат. Разберём каждый из этапов подробно.

Шаг 1. Прописываем логику. Для лучшей организации кода в проекте принято выносить всю логику Redux в отдельную папку.

Создайте внутри папки src/ папку store/ с файлом index.js. Откройте его и вставьте код из блока ниже. Здесь мы используем функции Redux Toolkit configureStore() для создания хранилища и createSlice() для редьюсера.

// src/store/index.js
// Здесь мы создаём Redux Slice и Redux Store
import { configureStore, createSlice } from '@reduxjs/toolkit';

// 1. Создаём срез состояния (Slice) с помощью createSlice
// Эта функция позволяет определить начальное состояние и редьюсеры и автоматически генерирует action creators в одном месте
const bankSlice = createSlice({
  name: 'bank', // Уникальное имя этого среза. Используется для Action Type, например: bank/deposit
  initialState: {
    balance: 0, // Начальное значение баланса
  },
  reducers: {
    // В Redux Toolkit можно писать мутабельный код внутри редьюсеров
    // Благодаря встроенной библиотеке Immer, RTK сам превратит его в иммутабельное обновление состояния под капотом, обеспечивая безопасность
    deposit: (state, action) => {
      state.balance += action.payload; // Выглядит как прямое изменение, но безопасно!
    },
    withdraw: (state, action) => {
      state.balance -= action.payload; // Выглядит как прямое изменение, но безопасно!
    },
    // Здесь могли бы быть и другие редьюсеры, например toggleInternet: (state) => { state.isInternetActive = !state.isInternetActive; },
  },
});

// Экспортируем наши функции-генераторы действий (action creators), которые были автоматически созданы createSlice
export const { deposit, withdraw } = bankSlice.actions;

// 2. Создаём Redux Store с помощью configureStore
// configureStore — это обёртка над createStore, которая автоматически настраивает Redux DevTools, добавляет middleware (например, thunk) и упрощает объединение редьюсеров
export const store = configureStore({
  reducer: {
    // Здесь мы определяем, какие редьюсеры отвечают за какие части общего состояния
    // Ключ bank будет использоваться для доступа к состоянию этого среза: state.bank
    bank: bankSlice.reducer,
  },
});

Сравните с чистой Redux из предыдущего раздела: кое-что изменилось, а именно — появились две новые сущности:

  • createSlice(). Вместо ручного создания initialState, switch/case редьюсера и отдельных объектов action мы определяем всё это в одном месте. RTK сам создаст необходимые типы действий и функции-генераторы для них.
  • configureStore(). Это упрощённая версия createStore, которая позволяет легко комбинировать несколько редьюсеров для упрощения работы.

Шаг 2. Подключаем Redux к React. Redux Store готов. Следующий шаг — сделать его доступным для всех React-компонентов нашего приложения.

Откройте src/index.js. Не перепутайте его с src/store/index.js, с которым мы работали в предыдущем шаге! Вставьте в src/index.js код:

// index.js (или main.jsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux'; // Импортируем провайдер
import { store } from './store';        // Импортируем наш Store
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

Здесь используется <Provider> — специальный компонент Redux, который передаёт Redux Store во всё дерево вложенных компонентов. Благодаря этому любой компонент может получить доступ к состоянию и экшенам напрямую, без явной передачи state через пропсы от родителя к потомкам. Такой подход избавляет от prop drilling и упрощает работу с общим состоянием приложения по мере его роста.

В Props компонент может получить данные только от родительского компонента. <Provider> позволяет избежать этого, так как состояние доступно всем компонентам напрямую
Инфографика: Майя Мальгина для Skillbox Media

Шаг 3. Создаём компоненты интерфейса. Теперь создадим компонент, который будет отображать баланс и кнопки для взаимодействия с пользователем. Мы будем использовать хуки useSelector() для чтения состояния из Store и useDispatch() для отправки действий.

Откройте src/App.js и вставьте в него следующий код:

// src/App.jsx (или src/App.js)
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
// Импортируем action creators (deposit, withdraw) из нашего Redux Store
import { deposit, withdraw } from './store';

function App() {
  // 1. Получаем текущий баланс из Redux Store с помощью хука useSelector
  //   state здесь — это весь объект состояния Store
  //   Мы обращаемся к 'state.bank.balance', потому что наш редьюсер был передан в configureStore под ключом bank
  const balance = useSelector((state) => state.bank.balance);

  // 2. Получаем функцию dispatch с помощью хука useDispatch
  //   Эта функция нужна для отправки (диспетчеризации) наших действий в Store
  const dispatch = useDispatch();

  return (
    <div className="App" style={appContainerStyle}>
      <h1>Банк Redux Toolkit</h1>
      <h2>Ваш баланс: {balance}$</h2>

      <div className="buttons">
        <button
          onClick={() => dispatch(deposit(100))} // Отправляем действие "deposit" с суммой 100
          style={depositButtonStyle}
        >
          Пополнить на 100$
        </button>

        <button
          onClick={() => dispatch(withdraw(50))} // Отправляем действие "withdraw" с суммой 50
          style={withdrawButtonStyle}
        >
          Снять 50$
        </button>
      </div>
    </div>
  );
}

export default App;

const appContainerStyle = {
  textAlign: 'center',
  fontFamily: 'Arial, sans-serif',
  padding: '20px',
  maxWidth: '500px',
  margin: '50px auto',
  border: '1px solid #eee',
  borderRadius: '8px',
  boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
  backgroundColor: '#fff',
};

const baseButtonStyle = {
  margin: '10px',
  padding: '10px 20px',
  fontSize: '16px',
  cursor: 'pointer',
  border: 'none',
  borderRadius: '5px',
  color: 'white',
  fontWeight: 'bold',
};

const depositButtonStyle = {
  ...baseButtonStyle,
  backgroundColor: '#4CAF50', // Зелёный для пополнения
};

const withdrawButtonStyle = {
  ...baseButtonStyle,
  backgroundColor: '#f44336', // Красный для снятия
};

Здесь мы тоже кое-что улучшили по сравнению с чистой Redux из прошлого раздела. Добавилось несколько сущностей:

  • useSelector(). Теперь мы обращаемся к state.bank.balance, потому что наш редьюсер был передан в configureStore() под ключом bank. Если бы у нас было несколько срезов (например, bankSlice и userSlice), мы бы обращались к state.bank или state.user.
  • Диспетчер с action creators. Вместо ручного создания объекта { type: «DEPOSIT», payload: 100 } мы просто вызываем функцию deposit(100), а RTK сам генерирует за нас правильный объект действия.

Шаг 4. Запускаем и проверяем приложение. Запустите проект, находясь в корневой папке терминала:

npm start

Теперь откройте приложение по адресу http://localhost:3000/. Всё работает: кнопки позволяют менять баланс, и он отображается на экране.

Интерфейс веб-приложения «банка»
Скриншот: Google Chrome / Skillbox Media

Приложение получилось небольшим, но демонстрирующим преимущества Redux:

  • Баланс хранится в одном-единственном объекте. В программе нет локальных состояний с балансом в компонентах, которые могли бы рассинхронизироваться и привести к его неверному отображению.
  • Ничто не может менять баланс напрямую — вместо этого вы отправляете действия, запускающие его обновление.
  • Приложение строго следует циклу «Событие → Действие → Обработка → Обновление → Отображение», что делает приложение предсказуемым и простым для отладки.

Что дальше

Теперь вы освоили основы Redux. Чтобы их закрепить и узнать о других возможностях библиотеки, попробуйте написать несколько дополнительных модулей для приложения. Например, можно добавить функциональность регистрации и авторизации или отображение истории транзакций. Перед этим загляните на официальный сайт Redux Toolkit — там есть разделы с примерами и гайдами, которые мы рекомендуем изучить для углублённого понимания.

А ещё RTK с React могут быть полезны в бэкенд-разработке: в Redux Toolkit есть инструмент для управления состоянием загрузки, ошибками и кешированием — RTK Query.

Больше интересного про код — в нашем телеграм-канале.  Подписывайтесь!





Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Курс с трудоустройством: «Профессия Разработчик + ИИ» Узнать о курсе
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована