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() });
}
}
Схематически этот процесс можно показать так:
Изучим еще один пример с использованием массива и передачей данных через действие:
// В свойстве 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)
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.