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';
// Редьюсер – функция, которая описывает то, как изменяются данные внутри контейнера
// Она принимает на вход текущее состояние приложения и должна вернуть новое
// Именно так работает функция 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 – функция, которая вызывает редьюсер
// передает туда текущее состояние
// и действие (напомню что это объект со свойством 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, где чтение состояния и его обновление находится снаружи.
Посмотрим ещё один пример с использованием массива и передачей данных через действие:
// 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 = /* ... */;
store.dispatch(addUser(user));
Несмотря на то, что ключ payload
необязательный и можно все данные складывать прямо в само действие, крайне рекомендуется так не делать. Мешать в одном объекте статически заданные ключи с динамическими плохая идея. Кроме того, в будущем мы будем использовать библиотеки, которые требуют именно такого способа работы.
Для написания самой простой версии Redux, нужно всего 7 строчек. Вот они:
// Второй параметр – начальное состояние данных внутри контейнера
const createStore = (reducer, initialState) => {
let state = initialState;
return {
dispatch: action => { state = reducer(state, action) },
getState: () => state,
}
}
Подведём итог. Что главное в Redux:
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.
Вам ответят команда поддержки Хекслета или другие студенты.
Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Загляните в раздел «Обсуждение»:
Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт