Redux

Redux — это такая база данных в программе. Она хранит внутри себя состояние (то есть данные) приложения. Redux отвечает только за состояние и никак не связан с браузером, DOM и фронтендом в целом. Его можно использовать, самого по себе, даже на бекенде в Node.js.

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

В простейшем случае для решения подобной задачи подошел бы и обычный объект JavaScript:

// Пример состояния и его инициализации
const state = { posts: [], activePostId: null, categories: [] };

// Где-то внутри приложения
state.posts.push(/* новый пост */);

// Где-то в другой части
state.posts.map(/* логика обработки */);

Но такой подход не позволяет отслеживать изменение данных. Если какая-то часть приложения изменила их, то мы об этом не узнаем, а значит не сможем отреагировать, например перерисовав нужную часть экрана. Redux решает эту проблему. Изменение данных внутри контейнера порождает события, на которые можно подписываться и выполнять произвольную логику (обычно перерисовку экрана). Достигается это за счет того, что данные внутри Redux изменяются не напрямую, как в случае обычного объекта, а через указание "действий" (actions).

Ниже полный пример использования Redux:

import { createStore } from 'redux';

// Редьюсер – функция, которая описывает то, как изменяются данные внутри контейнера
// Она принимает на вход текущее состояние приложения и должна вернуть новое
// Именно так работает функция reduce, отсюда и название
// Второй параметр описывает действие, с его помощью мы узнаем
// как конкретно надо обновить данные для конкретного вызова
// action — это объект, в котором обязательно есть поле type, содержащее имя действия
const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default: // действие по умолчанию – возврат текущего состояния
      return state;
  }
};

// Создание контейнера на основе редьюсера
// Именно в этом контейнере хранится состояние, которое возвращает редьюсер
const store = createStore(reducer);

// Состояние можно извлечь с помощью функции getState()
store.getState(); // 0 – так как это начальное значение состояния

// Функция subscribe позволяет подписываться на изменение состояния контейнера
// Она очень похожа на addEventListener, но без указания события
// Как только меняется любая часть состояния, контейнер по очереди вызывает все что было добавлено
// Здесь мы просто извлекаем из контейнера состояние и печатаем его на экран
store.subscribe(() => console.log(store.getState()));

// dispatch – функция, которая вызывает редьюсер
// передает туда текущее состояние
// и действие (напомню что это объект со свойством type)

// Редьюсер увеличивает состояние на единицу 
store.dispatch({ type: 'INCREMENT' }); // => 1
// Редьюсер увеличивает состояние на единицу 
store.dispatch({ type: 'INCREMENT' }); // => 2
// Редьюсер уменьшает состояние на единицу 
store.dispatch({ type: 'DECREMENT' }); // => 1

store.getState(); // 1

// Для избежания дублирования и повышения уровня абстракции, вынесем действия в функции
const increment = () => ({ type: 'INCREMENT' });
const decrement = () => ({ type: 'DECREMENT' });

store.dispatch(increment()); // => 2
store.dispatch(decrement()); // => 1

Единственный способ произвести изменения состояния в хранилище — это передать/отправить действие (action) в функцию dispatch. Действие — обычный JS-объект, в котором присутствует минимум одно свойство — type. Никаких ограничений на содержимое этого свойства не накладывается, главное, чтобы внутри контейнера был подходящий ему обработчик (в switch).

Сам процесс изменения состояния описан внутри контейнера, а снаружи мы лишь говорим, какое изменение необходимо выполнить. Этот подход резко отличается от того, как мы делали в React, где чтение состояния и его обновление находится снаружи.

Отправка действия (Redux)

Посмотрим ещё один пример с использованием массива и передачей данных через действие:

// payload - свойство внутри которого хранятся данные
const addUser = (user) => ({ type: 'USER_ADD', payload: { user } });

const reducer = (state = [], action) => { // инициализация состояния
  switch (action.type) {
    case 'USER_ADD': {
      const user = action.payload.user; // данные
      // как в редьюсере возвращаются новые данные без изменения старых
      return [...state, user];
    }
    case 'USER_REMOVE': {
      const id = action.payload.id; // данные
      return state.filter(u => u.id !== id); // данные не меняются
    }
    default:
      return state;
  }
};

const user = /* ... */;
store.dispatch(addUser(user));

Несмотря на то, что ключ payload необязательный и можно все данные складывать прямо в само действие, крайне рекомендуется так не делать. Мешать в одном объекте статически заданные ключи с динамическими плохая идея. Кроме того, в будущем мы будем использовать библиотеки, которые требуют именно такого способа работы.

Устройство Redux

Для написания самой простой версии Redux, нужно всего 7 строчек. Вот они:

// Второй параметр – начальное состояние данных внутри контейнера
const createStore = (reducer, initialState) => {
  let state = initialState;
  return {
    dispatch: action => { state = reducer(state, action) },
    getState: () => state,
  }
}

Три принципа

Подведём итог. Что главное в Redux:

  • Single source of truth — используя Redux, мы работаем только с одним контейнером на приложение. Это одно из ключевых отличий от Flux-архитектуры. Всё состояние в одном месте.
  • State is read-only — данные меняются только косвенно, используя функциональный стиль.
  • Changes are made with pure functions — внутри хранилища можно использовать только чистые функции. Тут правила даже строже чем во Flux, так как не позволяется использовать даже Date.now() и ему подобные функции, которые хотя и не обладают побочными эффектами, но все же являются недетерминированными. Все подобные вызовы должны делаться до вызова dispatch (подробнее об этом далее).

Начальное состояние

Выше упоминалось, что начальное состояние задаётся в определении редьюсера:

const reducer = (state = 0, action) { /* ... */ }

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

const store = createStore(reducer, initState);
// @@redux/INIT

Redux посылает специальное действие, которое нельзя перехватывать. Если редьюсер реализован правильно и содержит секцию default в switch, то контейнер заполнится данными из initState. Пример:

// const reducer = (state = 0, action) => {
//   switch (action.type) {
//     case 'INCREMENT':
//       return state + 1;
//     case 'DECREMENT':
//       return state - 1;
//     default:
//       return state;
//   }
// };
const store = createStore(reducer, 100);

store.getState(); // 100

В коде выше, функция createStore вызовет редьюсер так: reducer(100, '@@redux/INIT'). Затем выполнится ветка default и состоянием контейнера станет число 100.

Для полного доступа к курсу, нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам Хекслета, даст возможность обращаться за помощью к менторам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».