Зарегистрируйтесь, чтобы продолжить обучение

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