Урок 2: Основные концепции Redux: Store, Actions, Reducers

Приветствую всех на втором уроке нашего большого путешествия по Redux и Redux Toolkit. Сегодня мы перейдем от проблем к решениям и от теории к фундаментальным понятиям. Мы разберем те самые «три кита», на которых стоит весь Redux. Если на первом уроке мы увидели, почему в больших приложениях становится тесно с useState, то сегодня мы построим прочный фундамент для управления состоянием любого масштаба.

Представьте, что наше приложение это государство. В маленьком поселении (небольшом компоненте) все просто. Мэр (useState) знает обо всем и управляет всем на месте. Но когда поселение превращается в огромную страну (большое приложение), нужна централизованная система управления, конституция, законы и единый центр принятия решений. Redux это и есть такая система для вашего React-приложения.

В его основе лежат три простые концепции: Store (хранилище), Actions (действия) и Reducers (редьюсеры). Давайте разберем каждую из них так, чтобы вы не просто заучили определения, а почувствовали их взаимосвязь. Если кому интересно, можете также посмотреть уроки по React.

Store (хранилище)

Store это, без преувеличения, основа вашего Redux-приложения. Это безопасный сейф, единое и единственное место, где живет все состояние вашего приложения. Представьте его в виде большого JavaScript-объекта, который описывает ВСЕ, что сейчас происходит в вашем UI. Какие данные загружены, открыта ли модалка, какая тема оформления активна, какой пользователь авторизован.

Ключевая фраза здесь, это «единый источник истины». До Redux состояние могло быть размазано по десяткам компонентов на разных уровнях вложенности. Чтобы узнать, скажем, имя текущего пользователя, приходилось «копать» через пропсы нескольких компонентов. Теперь же ответ на любой вопрос о состоянии приложения находится в одном предсказуемом месте, в Store.

Это дает нам невероятные преимущества:

  1. Предсказуемость. В любой момент времени вы можете точно знать, каково состояние приложения. Его не могут тайно изменить в трех разных местах.

  2. Дебакгинг. Если что-то пошло не так, мы всегда можем «заглянуть в сейф» и посмотреть, что там лежит.

  3. Согласованность. Все компоненты, которые подписаны на данные из Store, работают с одной и той же версией правды. Невозможно получить ситуацию, когда в одном компоненте отображается одно имя пользователя, а в другом, другое.

Store это объект с особыми полномочиями. Его нельзя изменить напрямую. Вы не можете просто написать store.userName = 'Максим'. Единственный способ изменить состояние, это отправить в Store формальное «уведомление» (Action). И вот о них мы поговорим дальше.

Actions (действия)

Action это простой JavaScript-объект, который описывает событие, произошедшее в приложении. Если Store это сейф с состоянием, то Action это официальная заявка на его изменение. Action не содержит логики как изменить состояние, он лишь сообщает что случилось.

Представьте, что вы начальник и ваш подчиненный (Reducer, о нем ниже) должен выполнить ваши поручения. Вы не будете кричать ему: «Эй, измени там что-нибудь!». Вы напишете официальную бумагу: «Заявление: сотрудник Иванов взял отгул 25 мая». Action это и есть такая официальная бумага.

Структура Action

У каждого action есть обязательное поле type. Это строка, которая уникально идентифицирует произошедшее событие. По общепринятому соглашению, type часто пишут в формате 'домен/событие'.

javascript
// Примеры actions
{ type: 'counter/incremented' }
{ type: 'todos/todoAdded', payload: 'Купить молоко' }
{ type: 'users/loginRequest', payload: { username: 'maxim', password: '123' } }
{ type: 'notification/hide' }

Поле payload является опциональным и несет в себе дополнительные данные, необходимые для выполнения действия. В примере с добавлением задачи payload это текст самой задачи.

Action это всего лишь информационный объект. Он ничего не делает сам по себе. Его нужно «отправить» в Store.

Reducers (редьюсеры)

Reducer это функция, которая определяет, как изменится состояние приложения в ответ на action. Она принимает два аргумента, текущее состояние (state) и объект действия (action). На основе типа (type) полученного action, редьюсер решает, как вычислить и вернуть новое состояние.

Ключевое слово здесь «вернуть». Reducer не изменяет переданное ему состояние. Вместо этого он возвращает совершенно новый объект состояния.

Свойства Reducer:

  • Чистота. Это самое основное правило. Reducer должен быть чистой функцией. Это означает:

    1. При одних и тех же аргументах (state и action) она ВСЕГДА возвращает один и тот же результат.

    2. Не имеет побочных эффектов. Внутри редьюсера нельзя делать AJAX-запросы, изменять глобальные переменные, работать с DOM, читать файлы и т.д. Только вычисления на основе входных данных.

  • Иммутабельность. Reducer не может изменять существующий state. Вместо этого он должен создать копию текущего состояния, внести в эту копию необходимые изменения и вернуть ее.

Давайте посмотрим на примере, как выглядит «голый» Redux-редьюсер для счетчика:

javascript
// Начальное состояние (initial state)
const initialState = {
  value: 0
};

// Редьюсер для счетчика
function counterReducer(state = initialState, action) {
  // Проверяем тип действия
  switch (action.type) {
    case 'counter/incremented':
      // Возвращаем НОВЫЙ объект состояния
      return {
        ...state,        // Копируем все поля текущего состояния
        value: state.value + 1 // Перезаписываем поле `value` новым значением
      };
    case 'counter/decremented':
      return {
        ...state,
        value: state.value - 1
      };
    case 'counter/incrementByAmount':
      return {
        ...state,
        value: state.value + action.payload
      };
    default:
      // Если action незнаком, возвращаем состояние без изменений
      return state;
  }
}

Обратите внимание на оператор распространения (...state). Он создает поверхностную копию объекта состояния. Это самый базовый способ соблюсти принцип иммутабельности. Позже, с Redux Toolkit, эта задача станет намного проще.

Схема потока данных в Redux

Теперь давайте соберем все три «кита» в одну слаженную систему. Понимание этого потока данных, это ключ к пониманию Redux. Запомните эту схему:

UI -> Dispatch (Action) -> Reducer -> Store -> UI

Давайте разберем ее на реальном сценарии: «Пользователь нажал кнопку «+1» в компоненте счетчика».

  1. UI (Компонент). Пользователь нажимает кнопку «+1» в интерфейсе.

  2. Dispatch (Action). Обработчик клика в компоненте «отправляет» action. Вызывается специальная функция dispatch({ type: 'counter/incremented' }), которая передает этот action в Store.

  3. Reducer. Store получает action и передает его (вместе с текущим состоянием) в соответствующий редьюсер, в нашем случае, в counterReducer. Редьюсер видит type: 'counter/incremented' и выполняет свою логику: return { ...state, value: state.value + 1 }.

  4. Store. Store сохраняет новое состояние, которое вернул редьюсер. Старое состояние полностью заменяется новым.

  5. UI (Компонент). Store уведомляет все компоненты, которые подписаны на его данные (в React с помощью хука useSelector) о том, что состояние изменилось. Наш компонент счетчика получает обновленное значение value и перерисовывается, отображая новое число.

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

Пишем логику Redux вручную

Мы пока не будем подключать React. Наша цель сейчас создать Store, редьюсер и посмотреть, как всё это работает в чистом JavaScript.

Задача 1: Редьюсер для списка задач (todos)

javascript
// Начальное состояние - пустой массив задач
const initialState = {
  list: [],
  filter: 'all' // all, completed, active
};

// Редьюсер для управления списком задач
function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded':
      // Генерируем простой ID (в реальном приложении это делает бэкенд или библиотека)
      const newId = state.list.length > 0 ? Math.max(...state.list.map(t => t.id)) + 1 : 1;
      const newTodo = {
        id: newId,
        text: action.payload,
        completed: false
      };
      return {
        ...state,
        list: [...state.list, newTodo] // Не push, а создаем новый массив!
      };

    case 'todos/todoToggled':
      return {
        ...state,
        list: state.list.map(todo => {
          // Если это не та задача, которую нужно изменить, возвращаем ее как есть
          if (todo.id !== action.payload) {
            return todo;
          }
          // Найденную задачу возвращаем с обновленным полем completed
          return {
            ...todo,
            completed: !todo.completed
          };
        })
      };

    case 'todos/filterChanged':
      return {
        ...state,
        filter: action.payload
      };

    default:
      return state;
  }
}

Задача 2: Создание Store и диспатч действий

В «голом» Redux есть функция createStore, которая принимает редьюсер.

javascript
// Импортируем функцию создания хранилища (в реальном проекте с RTK мы ее не используем напрямую)
const { createStore } = require('redux');

// Создаем Store, передав в него наш редьюсер
const store = createStore(todosReducer);

// Подписываемся на обновления Store (чтобы видеть логи в консоли)
store.subscribe(() => {
  console.log('Состояние обновлено:', store.getState());
});

// Теперь диспатчим действия!
console.log('Начальное состояние:', store.getState());

// Добавляем задачу
store.dispatch({
  type: 'todos/todoAdded',
  payload: 'Изучить Actions'
});
// В консоли увидим: Состояние обновлено: { list: [ {id: 1, text: "Изучить Actions", completed: false} ], filter: 'all' }

// Добавляем еще одну
store.dispatch({
  type: 'todos/todoAdded',
  payload: 'Написать первый Reducer'
});

// Отмечаем первую задачу выполненной
store.dispatch({
  type: 'todos/todoToggled',
  payload: 1 // ID первой задачи
});
// В консоли увидим, что у задачи с id=1 поле completed стало true

// Меняем фильтр
store.dispatch({
  type: 'todos/filterChanged',
  payload: 'completed'
});

Попробуйте запустить этот код в Node.js. Посмотрите, как последовательно, шаг за шагом, изменяется состояние в ответ на каждое действие. Это и есть вся суть предсказуемости Redux в действии.

Итоги второго урока

Итак, на этом уроке мы с вами разобрали концепции Redux:

  • Store — единый и неизменяемый напрямую сейф для состояния.

  • Actions — простые объекты-уведомления о событиях.

  • Reducers — чистые функции, которые по action вычисляют новое состояние.

Мы увидели, как данные путешествуют по кругу, от интерфейса к хранилищу и обратно. Это может показаться избыточным для простого счетчика, но представьте, что у вас 20 различных видов данных и 50 типов действий. Такой строгий порядок становится не роскошью, а необходимостью.

Домашнее задание

  1. Мысленное моделирование. Представьте интерфейс личного кабинета (профиль пользователя, список заказов, настройки). Опишите 3-5 возможных Actions (их type и структуру payload). Например { type: 'profile/avatarChanged', payload: 'https://...' }.

  2. Практика с кодом. Допишите редьюсер todosReducer, добавив обработку действия 'todos/todoDeleted', которое должно удалять задачу по ее id (переданному в payload). Не забудьте про иммутабельность, используйте filter для создания нового массива.

  3. Эксперимент. В коде из практической части попробуйте напрямую изменить store.getState().list.push(...). Убедитесь, что это не вызовет перерисовку (наш console.log из subscribe не сработает) и это является нарушением принципов Redux.

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

Не забывайте, что полный курс со всеми уроками, проектами и дополнительными материалами ждет вас тут: курс с уроками по Redux и Redux Toolkit для начинающих.

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий