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

Redux JS: Redux (React)

Redux — это такая база данных в программе. Она хранит внутри себя данные приложения или, как говорят, состояние. 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

Единственный способ произвести изменения состояния в хранилище — это передать/отправить действие (action) в функцию dispatch(). Действие — обычный JS-объект, в котором присутствует минимум одно свойство — 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, нужно всего 7 строчек. Вот они:

// Второй параметр – начальное состояние данных внутри хранилища
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 посылает специальное действие, которое нельзя перехватывать. Если редьюсер реализован правильно и содержит секцию 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.

Три принципа

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

  • Single source of truth — используя Redux, мы работаем только с одним хранилищем на приложение. Всё состояние в одном месте
  • 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 месяцев
с нуля
Старт 1 июня
профессия
от 10 080 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 1 июня

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

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

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

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