Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Redux React: Redux Toolkit

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

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

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

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

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

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

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

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

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

import { createStore } from 'redux';

// Функция reducer описывает изменение данных внутри хранилища
// Она принимает текущее состояние приложения и возвращает новое состояние

// Второй параметр описывает действие, по которому мы выясняем, как обновить данные для вызова

//Объект 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, которая вызывает редьюсер

// Редьюсер увеличивает состояние на единицу
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

Единственный способ изменить состояние в хранилище — это передать или отправить действие в функцию dispatch().

Действие — это обычный JavaScript-объект, в котором есть как минимум одно свойство type. Никаких ограничений на содержимое этого свойства не накладывается, но в switch внутри редьюсера должен быть подходящий обработчик.

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

// Пример изменения состояния в React
class Clock extends React.Component {
  handleClick() {
    this.setState({ date: new Date() });
  }
}

Схематически этот процесс можно показать так:

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

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

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

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

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

Ключ payload необязательный. Поэтому можно все данные складывать в объект под любыми свойствами, но мы не советуем так делать. Смешивать статически заданные и динамические ключи в одном объекте — это плохая идея.

Как устроен Redux

Чтобы написать самую простую версию Redux, нужно всего семь строчек:

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

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

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

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

// Второй параметр — начальное состояние при создании хранилища
const store = createStore(reducer, initState);
// @@redux/INIT

Redux посылает специальное действие с type равным '@@redux/INIT', которое нельзя перехватывать. Если редьюсер реализован правильно и содержит секцию 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, { type: '@@redux/INIT' });

Затем выполнится ветка default, и число 100 станет состоянием хранилища.

Выводы

Подведем итог и обсудим три важных принципа. Работая с Redux, мы:

  • Создаем одно хранилище на приложение, храним состояние в одном месте (Single source of truth)
  • Меняем состояние, посылая действие внутрь хранилища (State is read-only)
  • Используем только чистые функции внутри хранилища, благодаря этому мы можем «путешествовать во времени» (Changes are made with pure functions)

Дополнительные материалы

  1. Основные принципы Redux

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 25 апреля
профессия
от 9 900 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 25 апреля

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»