Redux. Простой как грабли
Мне уже доводилось заглядывать в репозиторий библиотеки redux, но откуда-то появилась мысль углубиться в его реализацию. Своим в некотором роде шокирующим или даже разочаровывающим открытием я хотел бы поделиться с сообществом.
TL;DR: базовая логика redux помещается в 7 строк JS кода.
О redux вкратце (вольный перевод заголовка на гитхабе):
Redux — библиотека управления состоянием для приложений, написанных на JavaScript.
Она помогает писать приложения, которые ведут себя стабильно/предсказуемо, работают на разных окружениях (клиент/сервер/нативный код) и легко тестируемы.
Я склонировал репозиторий redux, открыл в редакторе папку с исходниками (игнорируя docs, examples и прочее) и взялся за ножницы клавишу Delete:
… потому что мог. Ну или потому что поленился писать для них примеры. Но без корнер-кейсов они ещё менее интересны, чем то, что ждёт вас впереди.
А теперь давайте разберём то, что осталось
Пишем redux за 7 строк
Весь базовый функционал redux умещается в малюсенький файлик, ради которого вряд ли кто-нибудь будет создавать github репозиторий 🙂
Так устроен redux. 18 страниц вакансий на HeadHunter с поисковым запросом «redux» — люди, которые надеются, что вы разберетесь в 7 строках кода. Всё остальное — синтаксический сахар.
С этими 7 строками уже можно писать TodoApp. Или что угодно. Но мы быстренько перепишем TodoApp из документации к redux.
Уже на этом этапе я думал бросить микрофон со сцены и уйти, но show must go on.
Давайте посмотрим, как устроен метод.
combineReducers
Это метод, который позволяет вместо того, чтобы создавать один огромный reducer для всего состояния приложения сразу, разбивать его на отдельные модули.
Используется он так:
Дальше использовать этот store можно так же, как предыдущий.
Разница моего примера и описанного в той же документации к TodoApp довольно забавная.
В документации используют модный синтаксис из ES6 (7/8/∞):
и соответственно переименовывают todoReducer в todos и counterReducer в counter. И многие в своём коде делают то же самое. В итоге разницы нет, но для человека, знакомящегося с redux, с первого раза эта штука выглядит магией, потому что ключ части состояния (state.todos) соответствует функции, названной также только по желанию разработчика (function todos()<>).
Если бы нам нужно было написать такой функционал на нашем micro-redux, мы бы сделали так:
Этот код плохо масштабируется. Если у нас 2 «под-состояния», нам нужно дважды написать (state, action), а хорошие программисты так не делают, правда?
В следующем примере от вас ожидается, что вы не испугаетесь метода Object.entries и Деструктуризации параметров функции
Однако реализация метода combineReducers довольно простая (напоминаю, это если убрать валидацию и вывод ошибок) и самую малость отрефакторить на свой вкус:
Мы добавили к нашему детёнышу redux ещё 9 строк и массу удобства.
Перейдём к ещё одной важной фиче, которая кажется слишком сложной, чтобы пройти мимо неё.
applyMiddleware
middleware в разрезе redux — это какая-то штука, которая слушает все dispatch и при определенных условиях делает что-то. Логирует, проигрывает звуки, делает запросы к серверу,… — что-то.
В оригинальном коде middleware передаются как дополнительные параметры в createStore, но если не жалеть лишнюю строчку кода, то использование этого функционала выглядит так:
При этом реализация метода applyMiddleware, когда ты потратишь 10 минут на ковыряние в чужом коде, сводится к очень простой вещи: createStore возвращает объект с полем «dispatch». dispatch, как мы помним (не помним) из первого листинга кода, — это функция, которая всего лишь применяет редюсер к нашему текущему состоянию (newState = reducer(state, action)).
Так вот applyMiddleware не более чем переопределяет метод dispatch, добавляя перед (или после) обновлением состояния какую-то пользовательскую логику.
Возьмём, например, самый популярный middleware от создателей redux — redux-thunk
Его смысл сводится к тому, что можно делать не только
но и передавать в store.dispatch сложные функции
И теперь, когда мы выполним команду
я понимаю, что конструкция выглядит жутковато, но её тоже просто нужно вызвать пару раз с произвольными параметрами и вы осознаете, что всё не так страшно, это просто функция, возвращающая функцию, возвращающую функцию (ладно, согласен, страшно)
Напомню, оригинальный метод createStore выглядел так
То есть он принимал атрибуты (reducer, initialState) и возвращал объект с ключами < dispatch, getState >.
Оказалось, что реализовать метод applyMiddleware проще, чем понять, как он работает.
Мы берём уже реализованный метод createStore и переопределяем его возвращаемое значение:
Вывод
Под капотом redux содержатся очень простые логические операции. Операции на уровне «Если бензин в цилиндре загорается, давление увеличивается». А вот то, сможете ли вы построить на этих понятиях болид Формулы 1 — уже решайте сами.
Краткое руководство по Redux для начинающих
Авторизуйтесь
Краткое руководство по Redux для начинающих
Библиотека Redux — это способ управления состоянием приложения. Она основана на нескольких концепциях, изучив которые, можно с лёгкостью решать проблемы с состоянием. Вы узнаете о них далее, в этом руководстве по Redux для начинающих.
Примечание Вы читаете улучшенную версию некогда выпущенной нами статьи.
Содержание:
Когда нужно пользоваться Redux?
Redux идеально использовать в средних и крупных приложениях. Им стоит пользоваться только в случаях, когда невозможно управлять состоянием приложения с помощью стандартного менеджера состояний в React или любой другой библиотеке.
Простым приложениям Redux не нужен.
Использование Redux
Разберём основные концепции библиотеки Redux, которые нужно понимать начинающим.
Неизменяемое дерево состояний
В Redux общее состояние приложения представлено одним объектом JavaScript — state (состояние) или state tree (дерево состояний). Неизменяемое дерево состояний доступно только для чтения, изменить ничего напрямую нельзя. Изменения возможны только при отправке action (действия).
Действия
Действие (action) — это JavaScript-объект, который лаконично описывает суть изменения:
Типы действий должны быть константами
В простом приложении тип действия задаётся строкой. По мере разрастания функциональности приложения лучше переходить на константы:
и выносить действия в отдельные файлы. А затем их импортировать:
Генераторы действий
Генераторы действий (actions creators) — это функции, создающие действия.
Обычно инициируются вместе с функцией отправки действия:
Или при определении этой функции:
Редукторы
При запуске действия обязательно что-то происходит и состояние приложения изменяется. Это работа редукторов.
Что такое редуктор
Редуктор (reducer) — это чистая функция, которая вычисляет следующее состояние дерева на основании его предыдущего состояния и применяемого действия.
Чистая функция работает независимо от состояния программы и выдаёт выходное значение, принимая входное и не меняя ничего в нём и в остальной программе. Получается, что редуктор возвращает совершенно новый объект дерева состояний, которым заменяется предыдущий.
Чего не должен делать редуктор
Редуктор — это всегда чистая функция, поэтому он не должен:
Поскольку состояние в сложных приложениях может сильно разрастаться, к каждому действию применяется не один, а сразу несколько редукторов.
Симулятор редуктора
Упрощённо базовую структуру Redux можно представить так:
Состояние
Список действий
Редуктор для каждой части состояния
Редуктор для общего состояния
Хранилище
Хранилище (store) — это объект, который:
Хранилище в приложении всегда уникально. Так создаётся хранилище для приложения listManager:
Хранилище можно инициировать через серверные данные:
Функции хранилища
Прослушивание изменений состояния:
Поток данных
Поток данных в Redux всегда однонаправлен.
Передача действий с потоками данных происходит через вызов метода dispatch() в хранилище. Само хранилище передаёт действия редуктору и генерирует следующее состояние, а затем обновляет состояние и уведомляет об этом всех слушателей.
Советуем начинающим в Redux прочитать нашу статью о других способах передачи данных.
Redux + React: основы
Redux является предсказуемым контейнером состояния для JavaScript приложений. Это позволяет вам создавать приложения, которые ведут себя одинаково в различных окружениях (клиент, сервер и нативные приложения), а также просто тестируются.
Redux решает проблему управления состоянием в приложении, предлагая хранить данные в глобальном State, и централизованно изменяя его.
Установка
Reducer
Это функция, которая принимает на вход команды и изменяет state. Если тип action неизвестен, возвращаем state. Пример реализации на JavaScript:
Redux-store
Store содержит всё дерево состояний приложения. Единственный способ изменить состояние внутри него — отправить на него action.
createStore(reducer)
Store — это не класс. Это просто объект с несколькими методами. Чтобы создать его, передайте свою функцию в createStore
getState()
Возвращает текущее дерево состояний вашего приложения. Он равен последнему значению, которое возвращает store’s reducer.
dispatch(action)
store.dispatch(action) — отправляет команду, и это единственный способ вызвать изменение состояния store.
Store’s reducer будет вызываться с текущим getState() результатом и заданным action, синхронно. Его возвращаемое значение будет считаться следующим состоянием. Он будет возвращен с новым getState(), и слушатели изменений будут немедленно уведомлены.
subscribe(listener)
Добавляет слушателя изменений. Вызывается каждый раз, когда store может быть изменён.
Как это работает вместе
Actions Creators
В store может передаваться много данных, поэтому бывает удобно сделать функции создатели действий.
bindActionCreators()
Превращает объект, значения которого являются actions creators, в объект с теми же ключами, но с каждым action creator, заключенным в dispatch-вызов, чтобы их можно было вызывать напрямую.
Единственный вариант использования для bindActionCreators- это когда вы хотите передать actions creators в компонент, который не знает о Redux, и вы не хотите передавать dispatch или хранить Redux в нем.
Структура проекта
Если у много actions creators, разумно вынести их в отдельный файл, или папку. То же касается Reducer’а.
React-Redux
Provider
connect()
connect — это компонент высшего порядка (HOC), который создаёт новые компоненты.
Store
A store holds the whole state tree of your application. The only way to change the state inside it is to dispatch an action on it.
A Note for Flux Users
If you’re coming from Flux, there is a single important difference you need to understand. Redux doesn’t have a Dispatcher or support many stores. Instead, there is just a single store with a single root reducing function. As your app grows, instead of adding stores, you split the root reducer into smaller reducers independently operating on the different parts of the state tree. You can use a helper like combineReducers to combine them. This is similar to how there is just one root component in a React app, but it is composed out of many small components.
Store Methods
Store Methods
getState()
Returns the current state tree of your application. It is equal to the last value returned by the store’s reducer.
Returns
(any): The current state tree of your application.
dispatch(action)
Dispatches an action. This is the only way to trigger a state change.
The store’s reducing function will be called with the current getState() result and the given action synchronously. Its return value will be considered the next state. It will be returned from getState() from now on, and the change listeners will immediately be notified.
A Note for Flux Users
If you attempt to call dispatch from inside the reducer, it will throw with an error saying “Reducers may not dispatch actions.” This is similar to “Cannot dispatch in a middle of dispatch” error in Flux, but doesn’t cause the problems associated with it. In Flux, a dispatch is forbidden while Stores are handling the action and emitting updates. This is unfortunate because it makes it impossible to dispatch actions from component lifecycle hooks or other benign places.
In Redux, subscriptions are called after the root reducer has returned the new state, so you may dispatch in the subscription listeners. You are only disallowed to dispatch inside the reducers because they must have no side effects. If you want to cause a side effect in response to an action, the right place to do this is in the potentially async action creator.
Arguments
Returns
(Object † ): The dispatched action (see notes).
Notes
Middleware is created by the community and does not ship with Redux by default. You need to explicitly install packages like redux-thunk or redux-promise to use it. You may also create your own middleware.
Example
subscribe(listener)
Adds a change listener. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed. You may then call getState() to read the current state tree inside the callback.
You may call dispatch() from a change listener, with the following caveats:
The listener should only call dispatch() either in response to user actions or under specific conditions (e. g. dispatching an action when the store has a specific field). Calling dispatch() without any conditions is technically possible, however it leads to an infinite loop as every dispatch() call usually triggers the listener again.
The subscriptions are snapshotted just before every dispatch() call. If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on the dispatch() that is currently in progress. However, the next dispatch() call, whether nested or not, will use a more recent snapshot of the subscription list.
The listener should not expect to see all state changes, as the state might have been updated multiple times during a nested dispatch() before the listener is called. It is, however, guaranteed that all subscribers registered before the dispatch() started will be called with the latest state by the time it exits.
Arguments
Returns
(Function): A function that unsubscribes the change listener.
Example
replaceReducer(nextReducer)
Replaces the reducer currently used by the store to calculate the state.
It is an advanced API. You might need this if your app implements code splitting, and you want to load some of the reducers dynamically. You might also need this if you implement a hot reloading mechanism for Redux.
Dispatch redux что это
Примечания для пользователей Flux
getState() ¶
Возвращает текущее состояние вашего приложения. Оно равно последнему возвращенному значению из редьюсера стора.
Возвращает
(any) Текущее состояние вашего приложения.
dispatch(action) ¶
Отправляет экшен. Это единственный способ изменить состояние.
Функция редьюсера стора будет вызвана с текущим результатом getState() и переданным dispatch (action) синхронно. Возвращенное значения будет содержать следующие состояние. Оно будет возвращено из getState() сразу же и подписчики будут немедленно уведомлены.
Примечания для пользователей Flux
Если вы попытаетесь вызвать dispatch изнутри редьюсера, то возникнет ошибка «Редьюсеры не могут отправлять экшены». Это аналогично ошибке во Flux «Нельзя отправлять в середине отправки», но это не вызвано проблемами связанными с ним. Во Flux отправлять запрещено пока стор не обработает экшен и запустит обновление. Это сделано для того, чтобы сделать невозможным отправку экшенов из хуков жизненного цикла компонент или других слабых мест.
В Redux, подписки вызванные после корневого редьюсера возвращают новое состояние, поэтому вы можете отправлять в подписанных слушателей. Вам только запрещено отправлять внутри редьюсера, потому что они не должны вызывать побочных эффектов. Если вы хотите вызывать побочный эффект вызванный в ответ на экшен, то правильно это сделать в потенциально асинхронном генераторе экшенов.
Параметры
Возвращает
(Object) Посланный экшен
Заметки
Mидлвары созданы сообществом и не поставляются с Redux по умолчанию. Вы должны явно установить пакеты, такие как redux-thunk или redux-promise для их использования. Вы также можете создать свои собственные мидлвары.
Пример¶
subscribe(listener) ¶
Вы можете вызвать dispatch() из слушателя изменений со следующими оговорками:
Слушатель должен только вызывать dispatch() либо в ответ на действия пользователя, либо в определенных условиях (например, отправление экшена, когда стор имеет конкретное поле). Вызов dispatch() без каких-либо условий технически возможен, однако это приводит к бесконечному циклу, так как каждый вызов dispatch() обычно снова вызывает слушателя.
Параметры
Возвращает
(Function) Функция которая отписывает слушателя.
Примеры¶
replaceReducer(nextReducer) ¶
Заменяет редьюсер, который в настоящее время используется стором, чтобы вычислить состояние.
Это продвинутое API. Вам возможно понадобится это, если ваше приложение реализует разделение кода и вы хотите загрузить некоторые редьюсеры динамично. Вам также это может понадобиться, если вы реализуете горячий механизм перезагрузки для Redux.
Параметры
reducer (Function) Следующий редьюсер для стора который будет использован.




