Redux что это

Redux что это

С 0 до 1. Разбираемся с Redux

Когда вышла версия 1.0 Redux, я решил потратить немного времени на серию рассказов о моем опыте работы с ним. Недавно я должен был выбрать “реализацию Flux” для клиентского приложения и до сих пор с удовольствием работаю с Redux.

Почему Redux?

Redux позиционирует себя как предсказуемый контейнер состояния (state) для JavaScript приложений. Редакс вдохновлен Flux и Elm. Если вы раньше использовали Flux, я советую прочитать, что Redux имеет с ним общего в разделе «Предшественники» новой (отличной!) документации.

Redux предлагает думать о приложении, как о начальном состоянии модифицируемом последовательностью действий (actions), что я считаю действительно хорошим подходом для сложных веб-приложений, открывающим много возможностей.

Конечно, вы можете найти больше информации о Redux, его архитектуре и роли каждого компонента в документации.

Создаем список друзей с React и Redux

Сегодня мы сфокусируемся на пошаговом создании вашего первого приложения, использующего Редакс и Реакт: создадим простой список друзей с нуля.

Вы можете найти готовый код здесь.

Redux что это

Для кого?

Эта статья написана для людей, не имеющих опыта работы с Redux. Опыт разработки с Flux также не обязателен. Я буду давать ссылки на документы, когда мы будем сталкиваться с новыми понятиями.

1. Установка

Автор Redux, Даниил Абрамов, создал отличную сборку для разработки с React, Webpack, ES6/7 и React Hot Loader, которую вы можете найти здесь.

Есть сборки уже с установленным Redux, но, я думаю, важно понять роль каждой библиотеки.

Теперь вы можете открыть приложение по адресу http://localhost:3000. Как вы видите, «hello world» готов!

1.1 Добавим redux, react-redux и redux-devtools

Нам нужно установить три пакета:

1.2 Структура директорий

Хотя то, что мы будем делать, довольно просто, давайте создадим структуру директорий как для реального приложения.

Мы будет видеть более детально роль каждой из директорий, когда будем создавать приложение. Мы переместили App.js в директорию containers, так что нужно будет настроить импорт statement в index.js.

1.3 Подключаем Redux

Нам нужно включить devtools только для окружения разработки, так что модифицируем webpack.config.js как здесь:

Мы делаем здесь две вещи. Мы переопределяем createStore используя созданную функцию, которая позволяет нам применять множественные store enhancers, таких как devTools. Мы также включаем функцию renderDevTools, которая рендерит DebugPanel.

Сейчас нам нужно модифицировать App.js для соединения с redux. Для этого мы будем использовать Provider из react-redux. Это сделает наш экземпляр хранилища доступным для всех компонентов, которые располагаются в Provider компоненте. Не нужно беспокоится о странно выглядящей функции, ее цель использовать “контекст” функции Реакта для создания хранилища, доступного для всех детей (компонентов).

Для создания хранилища мы используем createStore функцию, которую мы определили в devTools файле, как map всех наших редьюсеров.

В нашем приложении App.js — внешняя обертка для Redux и FriendListApp — корневой компонент для нашего приложения. После создания простого ‘Hello world’ в FriendListApp.js, мы можем наконец запустить наше приложение с redux и devTools. Вы должен получить это (без стилей).

Redux что это

Хотя это просто ‘Hello world’ приложение, у нас включен Hot Reloading, т.е. вы можете модифицировать текст и получать автоматическое обновление на экране. Как вы можете видеть, devtools справа показывает пустые хранилища. Заполним их!

2. Создаем приложение

Теперь, когда сделаны все настройки, мы можем сфокусироваться на самом приложении.

2.1 Действия и генераторы действий

Как вы можете видеть, это очень выразительный путь определения области действий нашего приложения, которое будет позволять нам добавлять друзей, отмечать их как «избранных» или удалять их из нашего списка.

Генераторы действий — функции, которые создают действия. В Redux генераторы действий являются чистыми функциями, что делает их портативными и простыми для тестирования, т.к. они не имеют сайд-эффектов.

Мы поместим их в папку действий, но не забывайте, что это разные понятия.

Как видите, действия довольно минималистичны. Чтобы добавить элемент, мы сообщаем все свойства (здесь мы имеем дело только с name), а для других мы ссылаемся на id. В более сложном приложении, мы, возможно, имели бы дело с асинхронными действиями, но это тема для другой статьи…

2.2 Редьюсеры

Мы, для начала, определяем вид состояния нашего приложения в initialState :

Состоянием может быть все, что мы захотим, мы можем просто сохранить массив друзей. Но это решение плохо масштабируется, так что мы будем использовать массив id и map друзей. Об этом можно почитать в normalizr.

Теперь нам нужно написать актуальный редьюсер. Мы воспользуемся возможностями ES6 для задания аргументов по умолчанию для обработки случаев, когда состояние не определено. Это поможет понять как записать редьюсер, в данном случае я использую switch.

Если вы не знакомы с синтаксисом ES6/7, то возможно вам будет трудно это прочесть. Т.к. нам нужно вернуть новое состояние объекта, как правило используют Object.assign или Spread operator.

Что здесь происходит: мы определяем новый id. В реальном приложении мы, возможно, возьмем его с сервера или, как минимум, убедимся, что он уникальный. Затем мы используем concat чтобы добавить этот новый id в наш id-лист. Concat добавит новый массив и не изменит оригинальный.

Как вы можете видеть, несмотря на синтаксис, который может сначала смутить, логика проста. Вы задаете состояние и получаете назад новое состояние. Важно: ни в одной точке этого процесса не изменять предыдущее состояние.

Окей, давайте вернемся и создадим редьюсеры для двух других действий:

Вы также можете заметить, что spread оператор позволяет нам манипулировать только теми состояниями, которое нам нужно изменить.

Redux не важно, как вы храните данные, так что можно использовать Immutable.js.

Теперь вы можете поиграть с хранилищем минуя интерфейс, путем вызова dispatch вручную в нашем App.js.

Вы увидите в devTools действия, с ними можно поиграть в реальном времени.

Redux что это

3. Создаем интерфейс

Т.к. этот урок не об этом, я пропустил создание React-компонентов и сфокусировался только на Redax. Мы имеем три простых компонента:

В Redux считается хорошей практикой делать по возможности большинство компонентов “глупыми”. Т.е. чем меньше компонентов связаны с Redux, тем лучше.

Здесь FriendListApp будет единственным “умным” компонентом.

Это часть нового синтаксиса ES7, называемая декоратор. Это удобный способ вызова функции высшего порядка. Будет эквивалентна connect(select)(FriendListApp); где select — функция, которая возвращает то, что мы здесь сделали.

То, что случится дальше — стандартный подход для React. Мы привяжем функции к onClick, onChange или onKeyDown свойствам, чтобы обработать действия пользователя.

Если вы заинтересовались, как это сделать, ты можете посмотреть весь код.

Сейчас вы можете почувствовать магию работы redux/react приложения. Как изображено на GIF, вы логгируете все действия.
Разрабатывать удобней, когда ты можешь производить какие-то действия, находить баги, возвращаться, исправлять их и повторять уже исправленную последовательность…

Источник

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 что это

Всё, что находится в оперативной памяти компьютера и относится к работе программы, можно назвать состоянием приложения. Приложения могут быть составлены из блоков, модулей и компонентов. У каждого блока может быть своё собственное состояние. У выпадающего списка собственным можно считать состояние признака видимости опций. Это следствие того, что пока значение выпадающего списка не поменялось, поведение остальных частей приложения не изменится.

Глобальное состояние приложения — это часть состояния приложения, которое используется, учитывается и изменяется в разных частях, модулях, компонентах этого приложения.

Будем считать, что остальные части приложения меняют свое поведение в зависимости от того, какой пункт списка выбран. Это и есть пример глобального состояния.

Глобальное состояние должно быть доступно из любой части приложения, а при его изменении компоненты приложения должны произвести необходимые вычисления. Следовательно, нам необходимо правильно управлять глобальным состоянием.

Из чего состоит Flux-архитектура

Оглядимся вокруг и посмотрим на объекты материального мира. Взять хотя бы котика. Он разбудит хозяина, когда голодный. Он наступит на клавиатуру, если хочет, чтобы с ним поиграли. Он гуляет сам по себе.

Действия котика и его состояние составляют сущность его существа. Мы можем смоделировать такое поведение и в наших программах с помощью компонентов. Каждый компонент содержит необходимые данные и определяет методы их изменения. Такой компонентный подход может быть реализован в архитектуре MVC.

Когда объектов становится много, наступает хаос. В программе так тоже бывает. Когда компонентов в архитектуре MVC много, и каждый из них что-то сообщает остальным, а все в ответ на это что-то изменяют в своих состояниях и снова сообщают об этом другим компонентам — наступает хаос. А всё потому что у каждого компонента хранится часть состояния, которая важна не только ему, но и другим компонентам.

Во Flux-архитектуре такое глобальное состояние предложено вынести в хранилища, отделить его от компонентов. А изменение состояния производить через специальные ворота, называемые диспетчерами. В этом случае поток управления меняет направление: изменение состояния переходит от представлений к диспетчеру, а новое состояние предоставляется всем представлениям сразу.

Redux что это

Во Flux-архитектуре состояние является общим для многих представлений.

Вот небольшая иллюстрация этой идеи. В хранилище можно сохранить список сообщений с признаками — название чата и флаг «прочтено».

При этом, если пользователь отметит, что сообщения в конкретном чате «прочитаны», соседние представления обновятся не потому что их уведомил компонент Messages, а потому что изменилось глобальное состояние приложения.

Что такое Redux?

Flux-архитектура — это идея. Идею можно реализовать разными способами, и один из них — Redux.

Redux — это реализация Flux на JavaScript. Эту реализацию удобно применять в веб-приложениях для управления общим глобальным состоянием. У неё есть специальная привязка для использования с React, о которой мы поговорим позднее.

Redux удобно использовать, когда

Использовать Redux можно и в vanila javascript приложениях. Для этого надо подключить библиотеку, например, из CDN.

Познакомимся поближе с составными частями Redux.

Анатомия Redux — action, store, reducer, dispatch.

В этом разделе соберём небольшое приложение, чтобы посмотреть на то, как части Redux взаимодействуют между собой. Предположим, нашему приложению требуется следить за нажатиями мыши на экране и вести журнал.

Action

В нашем приложении объект, описывающий действие, будет содержать дату, время и координаты мыши. Этот объект можно создать с помощью функции-фабрики.

reducer

store

dispatch

На этом приложение закончено. Работающий пример и полный код примера можно найти на JSFiddle.

Всё то же самое, но в связке с React

Функции createMouseClickAction и reducer остаются без изменений.

Вместо createView мы создадим компонент View:

В этом коде сравните строки

из предыдущего примера.

Вместо прямого подключения обработчика событий используем useEffect для своевременного снятия обработчика.

Функции useSelector и useDispatch могут выполнять свою роль, только если мы правильно присоединим React-приложение к хранилищу store нашего Redux.

Вы помните, что в предыдущем приложении мы создавали хранилище так:

На этом всё — мы сделали такое же приложение, только на React. Собранное работающее приложение можно найти на JSFiddle.

В качестве самостоятельной работы попробуйте модифицировать пример так, чтобы в левой части страницы перечислялись записи от нажатии мыши в левой части экрана, а в правом view — правые клики.

Альтернативы Redux

Redux — далеко не единственная возможность управления глобальным состоянием.

В банковском приложении один из атрибутов глобального состояния — баланс клиентского счета. Очевидно, что источник этого значения не может находиться на стороне браузера, потому что правильное значение баланса — ответственность банковского сервера.

Правильно ли использовать в таком случае использовать Redux для хранения остатков по счетам? Похоже, что нет.

Redux — не лучшее решение для ситуации, когда глобальное состояние — более глобальное, чем окно браузера.

Поэтому следует знать о альтернативных возможностях управления состоянием

Выбирайте с умом и используйте тот способ, который лучше подходит для ваших задач.

На профессии «React-разработчик» научим и не такому

Источник

Краткое руководство по 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 всегда однонаправлен.

Redux что это

Передача действий с потоками данных происходит через вызов метода dispatch() в хранилище. Само хранилище передаёт действия редуктору и генерирует следующее состояние, а затем обновляет состояние и уведомляет об этом всех слушателей.

Советуем начинающим в Redux прочитать нашу статью о других способах передачи данных.

Источник

С 0 до 1. Разбираемся с Redux

Когда вышла версия 1.0 Redux, я решил потратить немного времени на серию рассказов о моем опыте работы с ним. Недавно я должен был выбрать “реализацию Flux” для клиентского приложения и до сих пор с удовольствием работаю с Redux.

Почему Redux?

Redux позиционирует себя как предсказуемый контейнер состояния (state) для JavaScript приложений. Редакс вдохновлен Flux и Elm. Если вы раньше использовали Flux, я советую прочитать, что Redux имеет с ним общего в разделе «Предшественники» новой (отличной!) документации.

Redux предлагает думать о приложении, как о начальном состоянии модифицируемом последовательностью действий (actions), что я считаю действительно хорошим подходом для сложных веб-приложений, открывающим много возможностей.

Конечно, вы можете найти больше информации о Redux, его архитектуре и роли каждого компонента в документации.

Создаем список друзей с React и Redux

Сегодня мы сфокусируемся на пошаговом создании вашего первого приложения, использующего Редакс и Реакт: создадим простой список друзей с нуля.

Вы можете найти готовый код здесь.

Redux что это

Для кого?

Эта статья написана для людей, не имеющих опыта работы с Redux. Опыт разработки с Flux также не обязателен. Я буду давать ссылки на документы, когда мы будем сталкиваться с новыми понятиями.

1. Установка

Автор Redux, Даниил Абрамов, создал отличную сборку для разработки с React, Webpack, ES6/7 и React Hot Loader, которую вы можете найти здесь.

Есть сборки уже с установленным Redux, но, я думаю, важно понять роль каждой библиотеки.

Теперь вы можете открыть приложение по адресу http://localhost:3000. Как вы видите, «hello world» готов!

1.1 Добавим redux, react-redux и redux-devtools

Нам нужно установить три пакета:

1.2 Структура директорий

Хотя то, что мы будем делать, довольно просто, давайте создадим структуру директорий как для реального приложения.

Мы будет видеть более детально роль каждой из директорий, когда будем создавать приложение. Мы переместили App.js в директорию containers, так что нужно будет настроить импорт statement в index.js.

1.3 Подключаем Redux

Нам нужно включить devtools только для окружения разработки, так что модифицируем webpack.config.js как здесь:

Мы делаем здесь две вещи. Мы переопределяем createStore используя созданную функцию, которая позволяет нам применять множественные store enhancers, таких как devTools. Мы также включаем функцию renderDevTools, которая рендерит DebugPanel.

Сейчас нам нужно модифицировать App.js для соединения с redux. Для этого мы будем использовать Provider из react-redux. Это сделает наш экземпляр хранилища доступным для всех компонентов, которые располагаются в Provider компоненте. Не нужно беспокоится о странно выглядящей функции, ее цель использовать “контекст” функции Реакта для создания хранилища, доступного для всех детей (компонентов).

Для создания хранилища мы используем createStore функцию, которую мы определили в devTools файле, как map всех наших редьюсеров.

В нашем приложении App.js — внешняя обертка для Redux и FriendListApp — корневой компонент для нашего приложения. После создания простого ‘Hello world’ в FriendListApp.js, мы можем наконец запустить наше приложение с redux и devTools. Вы должен получить это (без стилей).

Redux что это

Хотя это просто ‘Hello world’ приложение, у нас включен Hot Reloading, т.е. вы можете модифицировать текст и получать автоматическое обновление на экране. Как вы можете видеть, devtools справа показывает пустые хранилища. Заполним их!

2. Создаем приложение

Теперь, когда сделаны все настройки, мы можем сфокусироваться на самом приложении.

2.1 Действия и генераторы действий

Как вы можете видеть, это очень выразительный путь определения области действий нашего приложения, которое будет позволять нам добавлять друзей, отмечать их как «избранных» или удалять их из нашего списка.

Генераторы действий — функции, которые создают действия. В Redux генераторы действий являются чистыми функциями, что делает их портативными и простыми для тестирования, т.к. они не имеют сайд-эффектов.

Мы поместим их в папку действий, но не забывайте, что это разные понятия.

Как видите, действия довольно минималистичны. Чтобы добавить элемент, мы сообщаем все свойства (здесь мы имеем дело только с name), а для других мы ссылаемся на id. В более сложном приложении, мы, возможно, имели бы дело с асинхронными действиями, но это тема для другой статьи…

2.2 Редьюсеры

Мы, для начала, определяем вид состояния нашего приложения в initialState :

Состоянием может быть все, что мы захотим, мы можем просто сохранить массив друзей. Но это решение плохо масштабируется, так что мы будем использовать массив id и map друзей. Об этом можно почитать в normalizr.

Теперь нам нужно написать актуальный редьюсер. Мы воспользуемся возможностями ES6 для задания аргументов по умолчанию для обработки случаев, когда состояние не определено. Это поможет понять как записать редьюсер, в данном случае я использую switch.

Если вы не знакомы с синтаксисом ES6/7, то возможно вам будет трудно это прочесть. Т.к. нам нужно вернуть новое состояние объекта, как правило используют Object.assign или Spread operator.

Что здесь происходит: мы определяем новый id. В реальном приложении мы, возможно, возьмем его с сервера или, как минимум, убедимся, что он уникальный. Затем мы используем concat чтобы добавить этот новый id в наш id-лист. Concat добавит новый массив и не изменит оригинальный.

Как вы можете видеть, несмотря на синтаксис, который может сначала смутить, логика проста. Вы задаете состояние и получаете назад новое состояние. Важно: ни в одной точке этого процесса не изменять предыдущее состояние.

Окей, давайте вернемся и создадим редьюсеры для двух других действий:

Вы также можете заметить, что spread оператор позволяет нам манипулировать только теми состояниями, которое нам нужно изменить.

Redux не важно, как вы храните данные, так что можно использовать Immutable.js.

Теперь вы можете поиграть с хранилищем минуя интерфейс, путем вызова dispatch вручную в нашем App.js.

Вы увидите в devTools действия, с ними можно поиграть в реальном времени.

Redux что это

3. Создаем интерфейс

Т.к. этот урок не об этом, я пропустил создание React-компонентов и сфокусировался только на Redax. Мы имеем три простых компонента:

В Redux считается хорошей практикой делать по возможности большинство компонентов “глупыми”. Т.е. чем меньше компонентов связаны с Redux, тем лучше.

Здесь FriendListApp будет единственным “умным” компонентом.

Это часть нового синтаксиса ES7, называемая декоратор. Это удобный способ вызова функции высшего порядка. Будет эквивалентна connect(select)(FriendListApp); где select — функция, которая возвращает то, что мы здесь сделали.

То, что случится дальше — стандартный подход для React. Мы привяжем функции к onClick, onChange или onKeyDown свойствам, чтобы обработать действия пользователя.

Если вы заинтересовались, как это сделать, ты можете посмотреть весь код.

Сейчас вы можете почувствовать магию работы redux/react приложения. Как изображено на GIF, вы логгируете все действия.
Разрабатывать удобней, когда ты можешь производить какие-то действия, находить баги, возвращаться, исправлять их и повторять уже исправленную последовательность…

Источник

Введение в Redux & React-redux

Redux что это

Оглавление

Введение

Вот вы прочитали мою статью про React (если нет, то настоятельно рекомендую вам сделать это) и начали разрабатывать приложения на нём. Но что это? Вы замечаете, как с расширением вашего приложения становится всё сложнее следить за текущим состоянием, сложно следить за тем, когда и какие компоненты рендарятся, когда они не рендарятся и почему они не рендарятся, сложно следить за потоком изменяющихся данных. Для этого и есть библиотека Redux. Сам React хоть и лёгкий, но для комфортной разработки на нем нужно много чего изучить.

И сегодня мы разберём 2 библиотеки: Redux и React-redux. Для использования Redux’а вам не нужно скачивать дополнительных библиотек, но, если использовать его в связке с библиотекой React-redux разработка становится ещё удобнее и проще.

Все примеры из этой статьи вы можете найти в этом репозитории на Github. Там находится полностью настроенное приложение React с использованием Redux и React-redux. Вы можете использовать его как начальную точку для вашего проекта. Изменяйте названия файлов и добавляйте новые в этот репозитории для создания собственного приложения. Смотрите во вкладку релизы для того что бы найти разные версии приложения. Первая содержит приложение только с использованием Redux, второе с использованием Redux и React-redux.

Мотивация использования Redux

Механизм локального хранилища компонента, который поставляется вместе с базовой библиотекой (React) неудобен тем, что такое хранилище изолировано. К примеру, если вы хотите, чтобы разные независимые компоненты реагировали на какое-либо событие, вам придётся либо передавать локальное состояние в виде пропсов дочерним компонентам, либо поднимать его вверх до ближайшего родительского компонента. В обоих случаях делать это не удобно. Код становится более грязным, трудночитаемым, а компоненты зависимыми от их вложенности. Redux снимает эту проблему так как всё состояние доступно всем компонентом без особых трудностей.

Redux является универсальным средством разработки и может быть использован в связке с различными библиотеками и фреймворками. В этой же статье будет рассматривается использование Redux в React приложениях.

1. Установка Redux и начало работы

Используете ли вы Yarn или Npm, выполните одну из этих команд для установки Redux:

Скорее всего вы используете папку src в которой хранится ваша кодовая база. Файлы, связанные с redux принято хранить в отдельной папке. Для этого я использую папку /src/store в которой хранится всё то, что связано с Redux и хранилищем приложения. Вы можете назвать ее по другому или поместить в другое место.

Создайте базовую структуру для хранилища. Она должна выглядит примерно следующим образом:

.store
├── actionCreators
│ ├── action_1.js
│ └── action_2.js
├── actions
│ ├── action_1.js
│ └── action_2.js
├── reducers
│ ├── reducer_1.js
│ ├── reducer_2.js
│ └── rootReducer.js
├── initialState.js
└── store.js

Конечно здесь я использовал примитивные названия для файлов, это сделано для наглядности. В настоящем проекте так называть файлы не стоит.

2. Redux

2.1 createStore

Когда вы создали базовую структуру для работы с хранилищем Redux пришло время понять то как вы можете взаимодействовать с ним.

Глобальное хранилище приложения создаётся в отдельном файле, который как правило называется store.js:

2.2 reducer()

reducer — чистая функция которая будет отвечать за обновление состояния. Здесь реализовывается логика в соответствие с которой будет происходить обновление полей store.

Так выглядит базовая функция reducer:

Функция принимает значение текущего состояния и обьект события (action). Обьект события содержит два свойства — это тип события (action.type) и значение события (action.value).

К примеру если нужно обработать событие onChange для поля ввода то объект события может выглядеть так:

Некоторые события могут не нуждаться в передаче каких-либо значении. К примеру, обрабатывая событие onClick мы можем сигнализировать о том, что событие произошло, более никаких данных не требуется, а как на него реагировать будет описывать логика, заложенная непосредственно в сам компонент которой должен на него реагировать и частично в reducer. Но во всех случаях необходимо определять тип события. Редьюсер как бы спрашивает: что произошло? actio.type равен «ACTION_1» ага значит произошло событие номер 1. Дальше его нужно как то обработать и обновить состояние. То, что вернёт редьюсер и будет новым состоянием.

ACTION_1 и ACTION_2 это константы событий. По-другому Actions. Про них мы поговорим далее 2.5 Actions.

Как вы уже догадались store может хранить сложную структуру данных состоящих из набора независимых свойств. Обновление одного свойства оставит нетронутым другие свойства. Так из примера выше, когда происходит событие номер один (ACTION_1) обновляется поле номер один (value_1) в store при этом поле номер два (value_2) остаётся нетронутым. В общем механизм схож с методом this.setState().

2.3 dispatch()

Что бы обновить store необходимо вызвать метод dispatch(). Он вызывается у объекта store который вы создаёте в store.js. Этот объект принято называть store поэтому обновление состояния в моём случае выглядит так:

ACTION_1 это константа события о которой речь пойдет дальше (см. Actions).

Эта функция вызовет функцию reducer который обработает событие и обновит соответствующие поля хранилища.

2.4 actionCreator()

На самом деле передавать объект события напрямую в dispatch() является признаком плохого тона. Для этого нужно использовать функцию под названием actionCreator. Она делает ровно то что и ожидается. Создаёт событие! Вызов этой функции нужно передавать как аргумент в dispatch а в actionCreator передавать необходимое значение (value). Базовый actionCreator выглядит следующим образом:

Таким образом вызов dispatch должен выглядеть так:

С использованием actionCreator код становится более чистым.

2.5 Actions

actions это константы, описывающие событие. Обычно это просто строка с названием описывающее событие. К примеру константа описывающее событие номер один будет выглядеть следующем образом:

Опять же в проекте вам стоит называть константы в соответствии с событием, которое она описывает: onClick, createUserSesion, deleteItem, addItem и т.д. Главное, чтобы было понятно. Замете что я нигде не писал import поэтому не забудьте импортировать ваши константы перед их использованием. Потому что константы тоже принято разбивать на отдельные файлы храня их в специальной папке. Хотя некоторые хранят их в одном файле под названием actionTypes.js. Такое решение нельзя назвать не правильным, но и не идеальным.

2.6 getState()

С помощью dispatch() обновили, а как теперь посмотреть новое значение store? Ничего изобретать не нужно, есть метод getState(). Он также, как и метод dispatch вызывается на экземпляре объекта store. Поэтому для моего примера вызов

вернёт значение полей хранилища. К примеру что бы посмотреть значение поля value_1 необходимо будет вызвать

2.7 subscribe()

А как же узнать, когда состояние обновилось? Для этого есть метод subscribe(). Он также вызывается на экземпляре store. Данный метод принимает функцию, которая будет вызывается каждый раз после обновления store. Он как бы «подписывает» функцию, переданную ему на обновление. К примеру следующий код при каждом обновлении (при каждом вызове dispatch()) будет выводить новое значение store в консоль.

Этот метод возвращает функцию unsubscribe(). Которая позволяет «отписаться от обновления». К примеру если компонент удаляется из DOM стоит отписать его методы от обновления в componentWillUnmount(). Этот метод жизненного цикла вызывается при размонтировании компонента и это именно то место где стоит отписываться от обновления. Проще говоря в деструкторе.

2.8 combineReducers()

combineReducers() позволяет объединить несколько редьюсеров в один.

Если логика обновления компонентов довольно сложна и\или необходимо обрабатывать большое количество различных типов событий, то корневой reducer может стать слишком громоздким. Лучшим решением будет разбить его на несколько отдельных редьюсеров каждый из которых отвечает за обработку только одного типа событий и обновления определённого поля.

Когда вы разбиваете базовый редьюсер на несколько, то название каждого из них должно соответствовать полю которое он обновляет в store.

К примеру если редьюсер обновляет поле номер один, то он может выглядеть так:

Название редьюсера (value_1) показывает какое свойство он будет обновлять в store. Если переименуете его в value_2 то он станет обновлять value_2. Поэтому учтите это!

Когда используется единый редьюсер мы показываем какое поле хотим обновить:

Но когда вы разделили ваши редьюсеры вам нужно просто вернуть новое значение:

Поскольку здесь не требуется указывать которое из полей обновляет редьюсер ибо его название и есть поле которое он обновляет.

2.9 initialState

initialState — объект, представляющий начальное состояние хранилища. Он является вторым не обязательным аргументом метода createStore(). С созданием хранилища можно сразу объявить начальное состояние для его полей. Этот объект желательно создавать, даже в тех случаях, когда объявления начального состояния не требуется. Потому что этот объект помогает посмотреть на структуру хранилища и название его полей. Обычный объект initialState выглядит следующим образом:

В некоторых случаях (когда компонент сразу использует значение из store), его объявление может стать обязательным иначе вы получите ошибку: TypeError: Cannot read property ‘value_1’ of undefined.

Также редьюсеры всегда должны возвращать по дефолту текущее состояние. К примеру, если используется единый reducer то последнее значение в switch должно выглядеть так:

Если же вы разделяете редьюсеры на независимые функции, то он должен возвращать значение того свойства за которое он отвечает:

Также если вы не передаёте объект initialState в createStore вы можете вернуть его из редьюсера. В обоих случаях будет инициализировано начальное состояние для store.

3. React-redux

Казалось бы, у нас есть всё что бы использовать Redux. Но на деле использование его без пакета React-redux в React приложениях выглядит не очень красиво.

3.1 Provider

Для использование store в компоненте вам необходимо передавать его в пропсы:

И после использовать в компоненте: this.props.state. Для этого react-redux предостовляет метод Provider:

Также можно передать store напрямую в компонент, не оборачивая его в Provider и это будет работать. Но лучше всё-таки используйте Provider.

3.2 mapStateToProps()

Этот метод вызывается всякий раз, когда происходит обновление store и именно он передаёт необходимые свойства из store в компонент. К примеру компонент, должен реагировать и обновлять UI каждый раз, когда поле номер один (value_1) обновилось. На обновление других полей ему реагировать не нужно. Если вы не используете React-redux вам бы пришлось использовать метод subscribe() что бы узнавать об обновлении и далее каким то образом проверять обновилось ли поле номер один или нет. В общем несложно понять, что такой код будет выглядеть слишком грязным и избыточным. С помощью mapStateToProps() можно чётко определить какие поля интересуют компонент. И на какие поля он должен реагировать.

Возвращаясь к примеру выше, если компоненту один нужно получать поле номер один (value_1) то mapStateToProps для него будет выглядеть следующим образом:

После внутри компонента мы можем обращается к полю value_1 через this.props.value_1. И каждый раз когда это поле будет обновляется компонент будет рендерится заново.

Вы можете создать отдельную папку в /src/store для хранения файлов каждый из которых будет содержать функцию mapStateToProps для всех ваших компонентов. Либо (как сделал это я) использовать единую функцию возвращающую функцию mapStateToProps для каждого компонента. Лично мне нравится такой подход. Такая функция выглядит следующим образом:

Эта функция в качестве аргумента принимает строку с названием компонента и возвращает функцию mapStateToProps которая возвращает объект со свойством из store необходимом для данного компонента. Эту функцию можно назвать mapStateToPropsGenerator().

3.3 mapDispatchToProps()

Эта функция передаёт в компонент методы для обновления необходимого поля store. Что бы не вызывать dispatch напрямую из компонента вы будете использовать данный метод для того что бы передавать в props метод вызов которого приведёт к вызову dispatch и обновлению соответствующего поля. Просто теперь это будет выглядеть более элегантно, а код более понятным и чистым.

К примеру компонент, номер один должен иметь возможность обновлять поле номер один из store. Тогда mapDispatchToProps для него будет выглядеть следующим образом:

Теперь для обновления свойства value_1 вы будете вызывать changeValue_1() через this.props.changeValue_1(value). Не вызывая dispatch напрямую через this.props.store.dispatch(action_1(value)).

bindActionCreators следует импортировать из redux. Он позволяет оборачивать функцию dispatch и actionCreator в единый объект. Вы можете не использовать bindActionCreators но тогда код будет выглядеть избыточным. Вы должны старятся реализовать какую-либо функциональность так, чтобы код выгладил просто и миниатюрно. Поэтому ничего лишнего писать не следует.

Только чистый и понятный код. Метод bindActionCreators(actionCreator, dispatch) принимает два обязательных параметра это функцию actionCreator о которой мы уже говорили и dispatch. Возвращая метод для изменения полей store.

Также как и для mapStateToProps я использую функцию генератор возвращающую функцию mapDispatchToProps для каждого компонента:

3.4 connect()

Ну и теперь кульминация! То без чего всё это не будет работать. Это функция connect.
Именно она связывает mapStateToProps и mapDispatchToProps с компонентом и передает необходимые поля и методы в него. Возвращает она новый компонент-обёртку для вашего компонента. Как правильно именовать такой компонент я не знаю, ибо в самой документации React-redux это не описывается. Лично я добавляю окончание _w для компонентов оберток. Как бы _w = wrap Component. Подключение компонента в этм случае выглядит так:

И теперь в ReactDOM.render() вы передаёте не ваш компонент, а тот что возвращает функция connect.

Если же у компонента нет необходимости в передаче ему mapStateToProps или mapDispatchToProps передавайте undefined или null в него.

Источник

Кому с Redux жить хорошо

Приветствую всех любителей хорошей инженерки! Меня зовут Евгений Иваха, я фронтенд-разработчик в команде, занимающейся дев-программой в ManyChat. В рамках дев-программы мы разрабатываем инструменты, позволяющие расширять функциональность ManyChat за счет интеграции со сторонними системами.

Существует мнение, что разработка через тестирование, или по канонам Test Driven Development (TDD) для фронтенда не применима. В данной статье я постараюсь развенчать этот миф и покажу, что это не только возможно, но и очень удобно и приятно.

Сам по себе React достаточно понятен любому разработчику, чего не скажешь про Redux. На первый взгляд может показаться, что это какой-то монструозный и непонятный инструмент. Прочитав данную статью, вы узнаете как разрабатывать приложения через тестирование на React, используя Redux, поймёте преимущества его использования, научитесь не открывать браузер при разработке фронтенд-приложений и экономить время на дебаге. Возможно, найдёте что-то новое для себя про написание фронтовых тестов.

Redux что это

Про TDD вообще и частный случай тестирования React-компонентов можно прочитать в статье моего коллеги Андрея, в данном материале я не буду заострять внимания на нюансах, но целиком и полностью пройду весь путь создания приложения через написание тестов.

Любой фронтенд-компонент продукта сам по себе делится на несколько составляющих. Для простоты можно считать, что в браузерном приложении есть свой фронтенд и свой бэкенд. Первый отвечает за непосредственный пользовательский интерфейс, а второй за бизнес-логику. React нам помогает с интерфейсами, а Redux представляет собой очень удобный инструмент для обработки бизнес-логики.

Поскольку, мы в ManyChat в начале 2020 года полностью перешли на TypeScript, будем писать код сразу с использованием строгой типизации.

Redux

Что такое Redux? Redux — это паттерн и библиотека для управления и обновления состоянием приложения с использованием специальных событий, называемых Action. Он предоставляет централизованное хранилище состояния, которое используется во всём приложении с правилами, гарантирующими предсказуемое изменение этого состояния. Если посмотреть на диаграмму потока данных в Redux для приложений на React, мы увидим примерно следующее:

Redux что это

При необходимости изменения состояния, например, при клике на элемент в DOM, вызывается Action creator, который создаёт определенный Action. Этот Action c помощью метода Dispatch отправляется в Store, где он передаётся на обработку в Reducers. Редьюсеры, в свою очередь, на основании текущего состояния и информации, которая находится в экшене, возвращают новое состояние приложения, которое принимает React с помощью Selectors для нового рендера DOM. Более подробно о каждом компоненте Redux будет рассказано ниже по ходу разработки приложения.

Такой однонаправленный поток данных даёт множество преимуществ, таких как — ослабление связанности компонентов, один источник информации о том, как должно выглядеть и действовать приложение, плюс разделение бизнес-логики и отображения, что приводит к значительному упрощению тестирования кода.

Задача

Создадим форму принятия какого-то пользовательского соглашения. Форма должна содержать чекбокс, который следует отметить пользователем, в случае, если он принимает соглашение, а так же кнопку Submit, которая становится активной только при активации чекбокса. Выглядеть это должно примерно вот так:

Redux что это

Воспользуемся шаблоном create-react-app:

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

Redux что это

Можем остановить сборщик, поскольку он понадобится нам ещё очень не скоро. Можем даже закрыть браузер.

Теперь приступим к самому вкусному — подключению Redux в наш проект. Т.к. приложение у нас совершенно пустое, мы не знаем структуру стора, оставим его напоследок.

Установим нужные пакеты:

Actions

Что такое Action? Это обычный Javascript объект, у которого есть обязательное свойство type, в котором содержится, как правило, осознанное имя экшена. Создатели Redux рекомендуют формировать строку для свойства type по шаблону домен/событие. Также в нём может присутствовать дополнительная информация, которая, обычно, складывается в свойство payload. Экшены создаются с помощью Action Creators — функций, которые возвращают экшены.

Здесь мы проверяем, Action Creator вернёт экшн с нужным типом и правильными данными, а именно — с названием чекбокса. И больше нам здесь нечего проверять.

Само собой, тест у нас красный (сломанный), т.к. код ещё не написан:

Redux что это

Пора написать код:

Redux что это

Тест пройден, можем приступить к рефакторингу. Здесь мы видим явное дублирование константы с типом экшена, вынесем её в отдельный модуль.

Убедимся, что тест позеленел, теперь поправим сам код:

Ещё раз убеждаемся, что тест проходит, и можем двигаться дальше.

Reducers

Напишем обработчик события изменения состояния чекбокса, т.е. Reducer. Редьюсеры — это специальные чистые функции, которые принимают на вход текущий state и action, решают как нужно изменить состояние, если это требуется, и возвращают новый state. Редьюсеры должны содержать всю бизнес-логику приложения, насколько это возможно.

Хранить состояние чекбоксов (отмечены они или нет) мы будем простым объектом, где ключом будет выступать название чекбокса, а в булевом значении непосредственно его состояние.

Приступим. Первый тест будет проверять, что мы получаем исходное состояние, т.е. пустой объект.

Т.к. у нас даже нет файла с редьюсером, тест сломан. Напишем код.

Первый тест редьюсера починили, можем написать новый, который уже проверит, что получим в результате обработки экшена с информацией о нажатом чекбоксе.

Минимальный код для прохождения данного теста будет выглядеть следующим образом:

Мы убедились, что при обработке экшена, в котором содержится имя чекбокса, в state будет состояние о том, что он отмечен. Теперь напишем тест, который проверит обратное поведение, т.е. если чекбокс был отмечен, то отметка должна быть снята, свойство должно получить значение false.

Отлично! Мы описали всю необходимую бизнес-логику в тестах, написали код, который этим тестам удовлетворяет.

Selectors

Чтобы получить состояние чекбокса, нам понадобится Selector. Селекторы — это функции, которые умеют извлекать требуемую информацию из общего состояния приложения. Если в нескольких частях приложения требуется одинаковая информация, используется один и тот же селектор.

Напишем первый тест для селектора.

Теперь заставим его «позеленеть».

Так как селектор должен знать, откуда извлекать информацию, определим структуру хранения.

Кажется, мы всё сделали правильно, тест теперь зелёный. Но что произойдёт, если у нас ещё не было информации о состоянии чекбокса? Напишем ещё один тест.

Получим вот такую картину:

Redux что это

Вот теперь селектор работает, как и требуется.

Store

Давайте теперь создадим сам Store, т.е. специальный объект Redux, в котором хранится состояние приложения.

Этот код мы отдельно тестировать не будем, т.к. мы используем стороннюю библиотеку, которая уже протестирована. Но далее мы напишем интеграционный тест, который проверит всю связку React + Redux.

React Components

Т.к. всю необходимую бизнес-логику мы уже описали, можем приступить к слою отображения.

Приступим к написанию тестов на компоненты. Начнём непосредственно с чекбокса.

Checkbox

Сделаем тест зелёным.

Заставим тест пройти.

Убедившись, что он красный, исправим код:

Тест красный, нужно снова поправить код.

Пора бы подключить Store к компоненту. Напишем тест, который покажет, что состояние чекбокса (свойство checked ) соответствует тому, что хранится в Store.

Тест пока красный, т.к. компонент ничего не знает о сторе. Заставим тест «позеленеть».

Redux что это

Дело в том, что ранее в тестах мы не передавали стор в компонент. Выделим часть с провайдером стора в функцию и перепишем наши тесты.

Функция renderWithRedux выглядит достаточно полезной, вынесем её в отдельный модуль и импортируем в тестах.

В итоге, шапка тестового файла будет выглядеть вот так:

Тест пройден, но у нас теперь появилось два теста с похожими описаниями и почти идентичным кодом, давайте немного модифицируем наши тесты, создав табличный тест. Последние два теста превратятся в один:

Так уже лучше. А теперь самое вкусное — напишем интеграционный тест, который проверит, что при нажатии на чекбокс, он изменит своё состояние, т.е. свойство checked.

AgreementSubmitButton

Нам требуется ещё один компонент — непосредственно кнопка Submit, создадим его. Конечно, вначале тест:

Теперь заставим тест «позеленеть»:

Тест зелёный, начало положено. Напишем новый тест, проверяющий зависимость свойства disabled новой кнопки от состояния чекбокса. Т.к. может быть два состояния, вновь используем табличный тест:

Имеем двойной красный тест, напишем код для прохождения этого теста. Компонент станет выглядеть вот так:

Ура, все тесты зелёные!

LicenseAgreement

Оформим нашу работу в то, ради чего мы всё это затевали — в форму принятия лицензионного соглашения. Какие имеются требования к форме:

Получили зеленый тест, можем написать второй для этого компонента. Файл с тестами изменится:

Чтобы он позеленел, добавим AgreementSubmitButton в компонент:

Заметим, что Checkbox и AgreementSubmitButton не зависят друг от друга. Каждый компонент зависит только от стора и больше ни от чего.

Ключ на старт!

Заставим тест позеленеть:

Redux что это

Это говорит о том, что мы не подключили Redux store в само приложение. Сделаем это в файле index.tsx :

Теперь приложение запускается, всё работает, как ожидается, кроме внешнего вида:

Redux что это

Исправим это, поправив вёрстку, и получим конечный результат:

Redux что это

Заключение

Я намеренно выбрал максимально простой пример, чтобы не перегружать статью текстом и кодом. Конечно, здесь можно было обойтись и без Redux, а хранить состояние чекбокса в локальном стейте компонента, и на мелких компонентах это может быть оправдано. Но по мере роста приложения, преимущества Redux будут становиться всё более очевидными, особенно когда появляется более сложная бизнес-логика, которую потребуется тестировать. Redux позволяет избавиться от прокидывания свойств через множество компонентов, каждый компонент либо достает необходимую информацию из стора самостоятельно, либо сам же диспатчит экшены.

Во второй части данной статьи предполагался рассказ о библиотеке Redux Tookilt, которая значительно упрощает использование Redux в разработке фронтенд-приложений, но я решил в следующей статье показать, как можно написать настоящее полезное приложение, хоть и очень простое, на React, Redux и Redux Toolkit.

Исходные коды полученного приложения доступны на GitHub.

Источник

Идиоматичный Redux: Дао Redux’а, Часть 1 — Реализация и Замысел

Мысли о том, какие требования выдвигает Redux, как задумано использование Redux и что возможно с Redux.

Введение

Я потратил много времени, обсуждая онлайн паттерны использования Redux, была ли это помощь тем, кто изучает Redux в Reactiflux каналах, дискуссии о возможных изменениях в API библиотеки Redux на Github’е, или обсуждение различных аспектов Redux’а в комментариях к тредам на Reddit’е или HN (HackerNews). С течением времени, я выработал свое собственное мнение о том, что представляет собой хороший, идиоматичный Redux код, и я хотел бы поделиться некоторыми из этих мыслей. Несмотря на мой статус мейнтейнера Redux’а, это всего лишь мнения, но я предпочитаю думать, что они являются достаточно хорошими подходами.

Redux, в своей сути, невероятно простой паттерн. Он сохраняет значение, выполняет одну функцию для обновления значения когда это необходимо, и уведомляет любых подписчиков о том, что что-то изменилось.

Несмотря на эту простоту, или, возможно, вследствие ее, существует широкий спектр походов, мнений и взглядов о том, как использовать Redux. Многие из этих подходов широко расходятся с концепциями и примерами из документации.

В то же время, продолжаются жалобы на то, как Redux «заставляет» вас делать вещи определенными способами. Многие из этих жалоб на самом деле включают концепции связанные с тем, как Redux обычно используется, а не фактическими ограничениями наложенными самой библиотекой Redux. (Например, только в одном недавнем HN треде я видел жалобы: «слишком много шаблонного кода», «константы action’ов и action creator’ы не нужны», «я вынужден редактировать слишком много файлов чтобы добавить одну фичу», «почему я должен переключаться между файлами чтобы добраться до своей логики?», «термины и названия слишком сложны для изучения или запутанны», и слишком много других.)

По мере того, как я исследовал, читал, обсуждал и изучал разнообразие способов использования Redux’а и идей, разделяемых в сообществе, я пришел к выводу, что важно различать то, как Redux на самом деле работает, задуманные способы его концептуального использования, и почти бесконечное количество способов возможного использования Redux. Я хотел бы затронуть несколько аспектов использования Redux и обсудить, как они вписываются в эти категории. В целом, я надеюсь объяснить, почему существуют конкретные паттерны и практики использования Redux, философию и замысел Redux’а, и то, что я считаю «идиоматичным» и «неидиоматичным» использованием Redux.

Этот пост будет разделен на две части. В «Часть 1 — Реализация и Замысел» мы рассмотрим фактическую реализацию Redux, какие конкретные ограничения он накладывает, и почему эти ограничения существуют. Затем, мы рассмотрим первоначальный замысел и проектные цели для Redux, основываясь на обсуждениях и заявлениях авторов (особенно на ранней стадии процесса разработки).

В «Часть 2 — Практика и Философия» мы исследуем распространенные практики, широко используемые в приложениях Redux, и опишем почему эти практики существуют в первую очередь. Наконец, мы рассмотрим ряд «альтернативных» подходов к использованию Redux и обсудим, почему многие их них возможны, но не обязательно «идиоматичны».

Изложение основ

Изучение Трех принципов

Начнем со взгляда на теперь известные Три Принципа Redux’а

В самом прямом смысле, каждое из этих заявлений — ложь! (или, заимствуя классическую реплику из «Возвращение джежая» — «они верны… с определенной точки зрения.»)

Но если эти заявления не полностью правдивы, зачем вообще они нужны? Эти принципы не фиксированные правила или буквальные заявления о реализации Redux’а. Скорее они формируют заявление о замысле того, как Redux следует использовать.

Эта тема будет продолжаться в остальной части текущей дискуссии. Из-за того, что Redux такая минимальная библиотека с точки зрения реализации, он так мало требует или навязывает на техническом уровне. Это поднимает ценную побочную дискуссию, на которую стоит взглянуть.

«Язык» или «Мета-язык»

В своей речи на ReactConf 2017 «Приручение Мета-языка» Ченг Лу описывает, что только исходный код является «языком», а все остальное, наподобие комментариев, тестов, документации, туториалов, блог постов, и конференций, является «мета-языком». Другими словами, исходный код сам по себе может передать только определенную часть информации. Много дополнительных слоев передачи информации на уровне человека требуется, чтобы люди понимали «язык».

Далее Ченг Лу продолжает обсуждать, как смещение дополнительных концепций в сам язык, позволяет выразить больше информации через медиум исходного кода, не прибегая к использованию «мета-языка» для передачи идей. С этой точки зрения, Redux — крошечный «язык» и почти вся информация о том, как его следует использовать является на самом деле «мета-языком».

«Язык» (в этом случае основная библиотека Redux) имеет минимальную экспрессивность, и следовательно концепции, нормы и идеи, окружающие Redux, все находятся на уровне «мета-языка». (Фактически, пост Понимание «Приручения Мета-языка», который раскладывает по полочкам идеи из выступления Ченга Лу, называет Redux конкретным примером этих идей.) В конечном счете, это означает, что понимание того, почему определенные практики существуют вокруг Redux’а, и решения о том, что является и не является «идиоматичным» будут включать мнения и обсуждения, а не просто определение, основанное на исходном коде.

Как Redux работает на самом деле

Прежде чем мы сильно углубимся в философскую сторону вещей, важно понимать какие технические ожидания у Redux’а действительно есть. Взгляд на внутренности и реализацию будет информативен.

Ядро Redux: createStore

Функция createStore центральная часть функциональности Redux’а. Если мы отсечем комментарии, проверку ошибок, и код для пары продвинутых возможностей, таких как store enhancers (усилители хранилища — функции, расширяющие возможности store — примечание переводчика) и observables, вот как выглядит createStore (пример кода позаимствован из «построй-мини-Redux» туториала под названием «Взламывая Redux»):

Это примерно 25 строк кода, но все же они включают ключевую функциональность. Код отслеживает текущее значение состояния и множество подписчиков, обновляет значение и уведомляет подписчиков когда action диспатчится, и предоставляет API для store.

Взгляните на все те вещи которые этот фрагмент не включает:

В этом ключе стоит процитировать pull-request Дэна Абрамова для примера «классический счетчик»:

Новый пример «классический счетчик» направлен на то, чтобы развеять миф о том, что Redux требует Webpack, React, горячую перезагрузку, саги, action creator’ы, константы, Babel, npm, CSS модули, декораторы, отличное знание латыни, подписки на Egghead, научную степень, или степень С.О.В. Нет, это всего лишь HTML, некоторые кустарные скрипт-тэги и старые добрые манипуляции с DOM. Наслаждайтесь!

Функция dispatch внутри createStore просто вызывает функцию-reducer и сохраняет любое возвращаемое значение. И все же, несмотря на это, элементы в том списке идей широко расцениваются, как концепции, о которых должно заботиться хорошее приложение на Redux.

Изложив все те вещи до которых createStore нет дела, важно отметить, что на самом деле эта функция требует. Настоящая функция createStore навязывает два конкретных ограничения: action’ы, которые доходят до store, обязаны быть простыми объектами, и action’ы обязаны иметь поле «type» не равное undefined.

Оба этих ограничения происходят от оригинальной концепции «Flux архитектуры». Цитируя секцию Flux Action’ы и Диспатчер из документации Flux:

Когда новые данные попадают в систему, как через человека, взаимодействующего с приложением, так и через web api вызов, эти данные упаковываются в action — объект, содержащий новые поля данных и конкретный action тип. Мы зачастую создаем библиотеку вспомогательных методов называемых ActionCreators которые не только создают объект action, но и передают action диспатчеру. Различные действия идентифицируются аттрибутом «type». Когда все store получают action, они обычно используют этот аттрибут для определения, следует ли им реагировать на него и каким образом. В приложении Flux, stor’ы и view контролируют сами себя; на них не воздействуют внешние объекты. Action’ы поступают в stor’ы через функции обратного вызова которые они определяют и регистрируют, а не методами установки (сеттерами).

Изначально Redux не требовал специальное поле «type», но позже была добавлена проверка валидности, чтобы помочь отловить возможные опечатки или неправильный импорт констант action’ов, и для избежания бесполезных споров о базовой структуре объектов.

Встроенная утилита: combineReducers

Здесь мы начинаем наблюдать некоторые ограничения, знакомые большему количеству людей. combineReducers ожидает, что каждый reducer среза, переданный в него, будет «корректно» реагировать на неизвестный action, возвращая свое состояние по-умолчанию и никогда не вернет undefined. Она также ожидает, что значением текущего состояния является простой JS объект, и что имеется точное соответствие между ключами в объекте текущего состояния и в объекте функции-reducer’а. И наконец, combineReducers выполняет проверку на равенство по ссылке, для определения все ли срез-reducer’ы вернули свое предыдущее значение. Если все вернувшиеся значения выглядят как предыдущие значения, combineReducers полагает, что ничего нигде не изменилось и, в качестве оптимизации, возвращает исходный корневой объект состояния.

Изначальное преимущество: инструменты разработчика Redux (DevTools)

Инструменты разработчика Redux состоят из двух основных частей: enhancer’а (усилителя) для store который реализует перемещение во времени путем отслеживания диспатченных action’ов, и пользовательского интерфейса, позволяющего просматривать и управлять историей. Сам по себе store enhancer не заботится о содержимом action’ов или состояния, он просто хранит action’ы в памяти. Изначально интерфейс инструментов разработчика нужно было рендерить внутри дерева компонентов вашего приложения, и он также не заботился о содержимом action’ов или состояния. Тем не менее, расширение Redux DevTools работает в отдельном процессе (по крайней мере в Chrome), и, следовательно, требует сериализуемости всех action’ов и состояния, для того, чтобы все возможности перемещения во времени работали корректно и быстро. Возможность импорта и экспорта состояния и action’ов также требует чтобы они были сериализуемыми.

Другое полу-требование для отладки с помощью перемещения во времени — иммутабельность и чистые функции. Если функция-reducer мутирует состояние, тогда переход между acton’ами в отладчике приведет к неконсистентным значениям. Если у reducer’а есть побочные эффекты, тогда эти побочные эффекты будут проявляться каждый раз когда DevTools повторяет action. В обоих случаях, отладка путем перемещения во времени не будет работать полностью как ожидается.

Основные связки с UI: React-Redux и connect

Настоящей проблемой мутация становится в функции connect из React-Redux. Оберточные компоненты, сгенерированные connect, реализуют множество оптимизаций для обеспечения того, чтобы обернутые компоненты ререндерились только тогда, когда на самом деле необходимо. Эти оптимизации вращаются вокруг проверок на ссылочное равенство, для определения того, изменились ли данные.

В частности, каждый раз, когда action диспатчится и подписчики уведомляются, connect проверяет, изменился ли корневой объект состояния. Если нет, connect предполагает что ничего в состоянии не изменилось, и пропускает дальнейшую работу по рендерингу (Вот почему combineReducers пытается, по мере возможности, вернуть тот же самый корневой объект состояния). Если же корневой объект состояния изменился, connect вызовет предоставленную функцию mapStateToProps, и выполнит неглубокую проверку на равенство между текущим результатом и предыдущим, для выявления изменились ли props, рассчитанные от данных store. Опять же, если содержимое данных выглядит одинаково, connect не будет ререндерить обернутый компонент. Эти проверки на равенство в connect‘е являются причиной того, почему случайные мутации состояния не приводят к ререндерингу компонентов, это из-за того, что connect предполагает, что данные не изменились и ререндеринг не нужен.

Сопутствующие библиотеки: React и Reselect

Иммутабельность также имеет значение и в других библиотеках, зачастую использующихся совместо с Redux. Библиотека Reselect создает запоминающие функции-селекторы, обычно используемые для извлечения данных из дерева состояния Redux. Запоминание значений, как правило, полагается на проверку ссылочного равенства, для определения того, совпадают ли входные параметры с ранее использованными.

Также, хотя React’овский компонент может реализовать shouldComponentUpdate, используя любую логику какую захочет, самая распространенная реализация полагается на неглубокие проверки равенства текущих props и новых входящих props, например:

В любом случае, мутация данных обычно приводит к нежелательному поведению. Запоминающие функции-селекторы не вернут правильные значения, и оптимизированные компоненты React’а не будут ререндерится когда должны.

Подводя итог техническим требованиям Redux

Центральная функция Redux’а createStore сама по себе накладывает только два ограничения на то, как вы должны писать свой код: action’ы должны быть простыми объектами, и должны содержать определенный type. Ее не заботит иммутабельность, сериализуемость, побочные эффекты или какое на самом деле принимает значение поле type.

С учетом вышесказанного, широко используемые части вокруг этого ядра, включая Redux DevTools, React-Redux, React и Reselect, действительно полагаются на правильное использование иммутабельности, сериализуемости action’ов/состояния и чистых функций-reducer’ов. Основная логика приложения может работать нормально если эти ожидания проигнорированы, но, с большой долей вероятности, отладка перемещением во времени и ререндеринг компонентов сломаются. Они также повлияют и на любые другие случаи использования, связанные с постоянством.

Важно отметить, что иммутабельность, сериализуемость и чистые функции никаким образом не навязываются Redux’ом. Функция-reducer вполне может мутировать свое состояние или выполнять AJAX-вызов. Любая другая часть приложения вполне может вызывать getState() и модифицировать содержимое дерева состояния напрямую. Полностью возможно помещать промисы, функции, Символы, инстансы класса или другие не сериализуемые значения в action’ы или дерево состояний. Вам не следует делать ничего из этого, но это возможно.

Замысел и дизайн Redux

Помня про эти технические ограничения, мы можем обратить внимание на то, как задумано использование Redux’а. Чтобы лучше понять эту мысль, полезно оглянуться на идеи, которые привели к начальной разработке Redux’а.

Влияние на Redux и его цели

Раздел «Введение» в документации Redux содержит несколько основных идей, повлиявших на разработку и концепции Redux, в темах Мотивация, Ключевые концепции и Предшественники. В качестве краткого итога:

Также стоит взглянуть на заявленные проектные цели в ранней версии README Redux’a.

Философия и проектные цели

Проектные принципы и подход

Прочитывая документацию Redux, ранние issue-треды, и многие другие комментарии Дэна Абрамова и Эндрю Кларка, можно заметить несколько конкретных тем касательно задуманного использования Redux.

Redux был создан как реализация архитектуры Flux

Redux изначально задумывался «всего лишь» как еще одна библиотека реализующая архитектуру Flux. В результате она уследовала многие коцепции от Flux: идею «отправки (dispatching) action’ов», то что action’ы — это простые объекты с полем type, использование «функций создания» action’ов (action creators), то, что «логика обновления» должна быть отделена от остальной части приложения и централизована, и многое другое.

Я часто вижу вопросы «Почему Redux делает ТАК», и на многие из подобных вопросов ответ: «Потому что Архитектура Flux’а и конкретные библиотеки Flux делали ТАК».

Поддерживаемость обновления состояния является главным приоритетом

Почти каждый аспект Redux’а, призван помочь разработчику понять когда, почему и как изменилась конкретная часть состояния. Это включает как фактическую реализацию, так и поощряемое использование.

Это значит, что разработчик должен иметь возможность посмотреть отправленный action, увидеть какие изменения состояния получились в результате, и вернуться к местам в кодовой базе, где этот action был диспатчен (особенно в зависимости от его типа). Если в store неверные данные, должно быть возможно отследить, какой диспатченный action привел к неправильному состоянию, и отмотать оттуда назад.

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

История action’ов должна иметь семантическое значение

Хотя ядро Redux’а не заботит какие конкретные значения содержатся в поле type ваших action’ов, довольно очевидно, что типы action’ов должны нести некоторый смысл и информацию. Redux DevTools и другие утилиты логирования отображают поле type для каждого диспатченного action’а, так что важно иметь значения, понятные с беглого взгляда.

Это значит, что строки полезнее Символов или чисел, с точки зрения передачи информации. Это также означает, что формулировка action типа должна быть ясной и понятной. Как правило, это означает, что наличие различных action типов будет понятнее разработчику, чем наличие только одного или двух action типов. Если во всей кодовой базе используется только один action тип (например, SET_DATA), то будет сложнее отслеживать откуда конкретный action был диспатчен, а жрунал истории действий будет менее читабельным.

Redux задуман для введения в принцины функционального программирования

Redux явно замыслен для использования с концепциями функционального программирования, и для того, чтобы помочь представить эти концепции как новым, так и опытным разработчикам. Эти концепции включают основы ФП, такие как: иммутабельность и чистые функции, а также идеи, наподобие композиции функций для решения большей задачи.

В то же время, Redux призван помочь обеспечить реальную ценность для разработчиков, пытающихся решать проблемы и создавать приложения, не подавляя при этом большим количеством абстрактных концепций ФП, и не увязая в спорах о терминах ФП, таких как «монады» или «эндофункторы». (Нужно признать, количество терминов и концепций вокруг Redux’а выросло с течением времени, и многие из них путают новичков, но цели: использования преимуществ ФП и введение в ФП для начинающих — явно были частью оригинального дизайна и философии.)

Redux поощряят тестируемый код

Наличие reducer’ов в качестве чистых функций позволяет выполнять отладку с переходом во времени, и также означает, что функция-reducer может легко быть протестирована в изоляции. Тестирование reducer’а должно требовать только его вызова с конкретными аргументами и проверки вывода — нет необходимости создавать моки для таких вещей как AJAX вызовы.

AJAX вызовы и другие побочные эффекты по-прежнему должны находится где-то в приложении, и тестирующий код, который их использует, может по-прежнему работать. Тем не менее, упор на чистые функции в значительной части кодовой базы снижает общую сложность тестирования.

Функции-reducer’ы следует организовывать по срезу состояния

Redux берет концепт индивидуальных «хранилищ» из архитектуры Flux и объединяет их в единственный store. Самым прямолинейным соответствием между Flux и Redux является создание отдельного ключа верхнего уровня или «среза» в дереве состояний для каждого Flux хранилища. Если Flux приложение имеет раздельные UsersStore, PostsStore и CommentsStore, эквивалент в Redux будет иметь корневое дерево состояний, выглядящее так: < users, posts, comments >.

Можно ограничиться единственной функцией, содержащей всю логику по обновлению всех срезов состояния, но любое осмысленное приложение захочет разбить такую функцию на более мелкие функции для облегчения сопровождения. Самым очевидным способом это сделать является раздление логики в зависимости от того, какой срез состояния должен быть обновлен. Это значит, что каждый «reducer среза» должен заботиться только о своем срезе состояния, и, насколько ему известно, этот срез может быть всем состоянием. Этот паттерн «композиции reducer’ов» можно многократно повторяться для обработки обновлений иерархической структуры состояния. И утилита combineReducers включена в состав Redux’а специально для упрощения использования этого паттерна.

Если каждую функцию-reducer среза можно вызывать отдельно и предоставлять ей только собственный срез состояния в качестве параметра, то это означает, что с одним и тем же action’ом можно вызывать несколько reducer’ов среза, и каждый из них может обновлять свой срез состояния независимо от других. Основываясь на заявлениях Дэна и Эндрю, возможность одного action’а привести к обновлениям нескольких reducer’ов среза, является ключевой особенностью Redux’а. Про это часто говорят: «action’ы имеют отношение 1-ко-многим с reducer’ами.»

Логика обновления и поток данных выражены явно

Redux не содержит никакой «магии». Некоторые аспекты его реализации (такие как applyMiddleware и store enhancers) чуть сложнее понять сразу, если вы не знакомы с более продвинутыми принципами ФП, но в остальном все должно быть явным, ясным и отслеживаемым с минимальным количеством абстракций.

Redux на самом деле даже не реализует саму логику обновления состояния. Он просто полагается на любую корневую функцию-reducer, которую вы предоставите. Он имеет утилиту combineReducers, чтобы помочь в распространненом случае — независимом управлении состояниями reducer’ами среза. Но вас полностью поощряют на написание собственной логики reducer’а для удовлетворения ваших потребностей. Также это означает, что ваша логика reducer’а может быть простой или сложной, абстрактной или многословной — все зависит от того, как вы хотите ее написать.

В оригинальном диспатчере Flux’а, Stor’ам нужно было событие waitFor(), которое могло быть использовано для задания цепочек зависимостей. Если CommentsStore нуждался в данных от PostsStore для того, чтобы правильно обновить себя, он мог вызвать PostsStore.waitFor(), чтобы гарантировать, что он выполнится после того как PostsStore обновится. К сожалению, такую цепочку зависимостей было нелегко визуализировать. Тем не менее, с Redux такая последовательность операций может быть достигнута явным вызовом конкретных функций-reducer’ов в нужной последовательности.

Вот в качестве примера некоторые (немного модифицированные) цитаты и сниппеты из Дэновского гиста «Combining Stateless Stores»

В этом случае, commentsReducer не зависит полностью от состояния и action’а. Он также зависит и от hasCommentReallyBeenAdded (былЛиКомментарийДействительноДобавлен). Мы добавляем этот параметр к его API. Да, его теперь нельзя использовать «как есть», но в этом весь смысл: reducer теперь имеет явную зависимость от других данных. Он не store верхнего уровня. То, что управляет им, обязано каким-то образом предоставить ему эти данные.

Это также применимо к идее «reducer’ов высшего порядка». Отдельный reducer среза может быть обернут другими reducer’ами, чтобы получить такие способности как отмена/повтор или пагинация.

API Redux’а должно быть минимальным

Эта цель повторялась снова и снова Дэном и Эндрю на протяжении разработки Redux’а. Проще всего процитировать некоторые из их комментариев:

Зачастую лучший API — это отсутствие API. Текущие предложения для middleware и stor’ов высшего порядка обладают огромным преимуществом в том, что они не требуют особого отношения со стороны ядра Redux — они просто обертки вокруг dispatch() и createStore() соответственно. Вы даже можете использовать их сегодня, до релиза 1.0. Это огромная победа для расширяемости и быстрых инноваций. Мы должны поддерживать паттерны и соглашения вместо жестких, привилегированных API.

С Redux я могу использовать простые объекты, массивы и что угодно для состояния.

Я изо всех сил старался избегать API наподобие createStore, потому что они привязывают вас к конкретной реализации. Вместо этого, для каждой сущности (Reducer, Action Creator) я попытался найти минимальный способ взаимодействия с ней без какой-либо зависимости от Redux. Единственный код, импортирующий Redux и действительно жестко зависимый от него, будет в вашем корневом компоненте и компонентах, подписанных на него.

Redux должен быть расширяем настолько, насколько возможно

Это связано с целью «минимального API». Некоторые библиотеки Flux, такие как Flummox Эндрю, имели некую форму асинхронного поведения, встроенную непосредственно в библиотеку (например, START/SUCCESS/FAILURE action’ы для промисов). И хотя наличие чего-то встроенного в ядро означало, что оно всегда доступно, это также ограничивало гибкость.

Снова проще всего процитировать комментарии из обсуждений и Hashnode AMA (вопросы/ответы) с Дэном и Эндрю:

Для продолжения поддержки асинхронных action’ов и для предоставления точки расширения для внешних плагинов и инструментов, мы можем предоставить некий общий action middleware, вспомогательную функцию для композиции middlewar’ов, и документацию для авторов расширений о том, как легко создавать свои собственные.

Я согласен, что это естественная возможность, которую наверняка захотят большинство Redux приложений, но как только мы встроим ее в ядро, все сразу начнут бессмысленные споры о том, как именно она должна работать, что собственно и случилось со мной и Flummox. Мы пытаемся держать ядро настолько малым и гибким насколько возможно, чтобы мы могли выполнять короткие итерации и позволять другим строить поверх него.

Как однажды сказал Дэн (не помню где… возможно в Slack) мы нацелены быть Koа для Flux библиотек. В конечном счете, когда сообщество повзрослеет, мы планируем поддерживать коллекцию «благословленных» плагинов и расширений, возможно в рамках reduxjs организации на Github’е.

Мы не хотели прописывать что-то подобное в самом Redux’е, потому что знаем, что многие люди не готовы изучать Rx операторы для выполнения базовых асинхронных операций. Они полезны, когда у вас сложная асинхронная логика, но мы не хотели вынуждать каждого пользователя Redux изучать Rx, так что мы намеренно поддержали гибкость middlewar’ов.

Причина, по которой middleware API вообще существует, в том, что мы совершенно не хотели останавливаться на конкретном решении для асинхронности. Моя предыдущая библиотека Flux — Flummox — имела, по сути, встроенный промис-middleware. Для кого-то это было удобно, но из-за того, что она была встроенной, вы не могли изменить ее поведение или отказаться от нее. С Redux мы знали, что сообщество придумает множество лучших асинхронных решений, чем то, что мы бы могли сделать самостоятельно. Redux Thunk рекламируется в документации потому, что это абсолютно минимальное решение. Мы были уверены, что сообщество придумает нечто другое и/или лучшее. Мы были правы!

Заключительные мысли

Я потратил много времени на исследование для этих двух постов. Было невероятно интересно прочитать ранние дискуссии и комментарии, и увидеть как Redux эволюционировал в то, что мы знаем теперь. Как видно из процитированного ранее README, видение Redux’а было ясным с самого начала, и было несколько конкретных идей и концептуальных скачков, которые привели к окончательному API и реализации. Надеюсь, этот взгляд на внутренности и историю Redux поможет пролить свет на то, как Redux работает на самом деле, и почему он был построен таким образом.

Обязательно ознакомьтесь с Дао Redux’а, Часть 2 — Практика и Философия, где мы взглянем на то, почему существует множество паттернов использования Redux, и я поделюсь своими мыслями о плюсах и минусах многих «вариаций» того, как можно использовать Redux.

У меня ушло достаточно много времени на перевод этого поста, пишите в комментариях если понравилось и есть желание прочитать перевод второй части, буду рад услышать ваше мнение.

Источник

Архитектура Redux. Да или нет?

Автор материала, перевод которого мы сегодня публикуем, говорит, что входит в команду мессенджера Hike, которая занимается новыми возможностями приложения. Цель этой команды заключается в том, чтобы воплощать в реальность и исследовать идеи, которые могут понравиться пользователям. Это означает, что действовать разработчикам нужно оперативно, и что им приходится часто вносить изменения в исследуемые ими новшества, которые направлены на то, чтобы сделать работу пользователей как можно более удобной и приятной. Они предпочитают проводить свои эксперименты с применением React Native, так как эта библиотека ускоряет разработку и позволяет использовать один и тот же код на разных платформах. Кроме того, они пользуются библиотекой Redux.

Redux что это

Когда разработчики из Hike начинают работу над чем-то новым, то, при обсуждении архитектуры исследуемого решения, у них возникает несколько вопросов:

Разделение ответственностей

Что такое «разделение ответственностей»? Вот что говорит об этом Википедия: «В информатике разделение ответственностей представляет собой процесс разделения компьютерной программы на функциональные блоки, как можно меньше перекрывающие функции друг друга. В более общем случае, разделение ответственностей — это упрощение единого процесса решения задачи путём разделения на взаимодействующие процессы по решению подзадач».

Архитектура Redux позволяет реализовать принцип разделения ответственностей в приложениях, разбивая их на четыре блока, представленные на следующем рисунке.

Redux что это
Архитектура Redux

Вот краткая характеристика этих блоков:

Что делать, если разным компонентам нужны одни и те же данные?

В приложении Hike есть экран, отображающий список друзей пользователя. В верхней части этого экрана выводится информация об их количестве.

Redux что это
Экран с информацией о друзьях в приложении Hike

Здесь имеется 3 React-компонента:

Совершенно очевидный способ создания подобной страницы приложения заключается в том, чтобы загрузить данные о друзьях в компонент-контейнер и передать их в виде свойств дочерним компонентам.

Подумаем теперь о том, что эти данные о друзьях могут понадобиться и в каких-то других компонентах, используемых в приложении.

Redux что это
Экран чатов в приложении Hike

Предположим, в приложении имеется экран чатов, который также содержит список друзей. Видно, что и на экране со списком друзей, и на экране чатов используются одни и те же данные. Как поступить в подобной ситуации? У нас есть два варианта:

Использование Redux

Здесь речь идёт об организации работы с данными с использованием хранилища, создателей действий, редьюсеров и двух компонентов пользовательского интерфейса.

▍1. Хранилище данных

Хранилище содержит загруженные данные о друзьях пользователя. Эти данные могут быть отправлены в любой компонент в том случае, если они там нужны.

▍2. Создатели действий

В данном случае создатель действия используется для диспетчеризации событий, направленных на сохранение и обновление данных о друзьях. Вот код файла friendsActions.js:

▍3. Редьюсеры

Редьюсеры ожидают поступления событий, представляющих диспетчеризованные действия, и обновляют данные о друзьях. Вот код файла friendsReducer.js:

▍4. Компонент, выводящий список друзей

Этот компонент-контейнер просматривает данные о друзьях и обновляет интерфейс при их изменении. Кроме того, он ответственен за загрузку данных из хранилища в том случае, если их у него нет. Вот код файла friendsContainer.js:

▍5. Компонент, выводящий список чатов

Этот компонент-контейнер так же пользуется данными из хранилища и реагирует на их обновление.

О реализации архитектуры Redux

Для того чтобы довести вышеописанную архитектуру до рабочего состояния может понадобиться день-два, но когда в проект нужно будет вносить изменения, производятся они очень просто и быстро. Если в приложение понадобится добавить новый компонент, использующий данные о друзьях, сделать это можно без необходимости беспокоиться о синхронизации данных или о том, что придётся переделывать другие компоненты. То же самое касается и удаления компонентов.

Тестирование

При использовании Redux каждый блок приложения поддаётся независимому тестированию.
Например, каждый компонент пользовательского интерфейса можно легко подвергнуть модульному тестированию, так как он оказывается независимым от данных. Речь идёт о том, что функция, представляющая такой компонент, всегда возвращает одно и то же представление для одних и тех же данных. Это делает приложение предсказуемым и снижает вероятность ошибок, возникающих при визуализации данных.

Каждый компонент можно всесторонне протестировать, используя самые разные данные. Такое тестирование позволяет выявить скрытые проблемы и способствует обеспечению высокого качества кода.

Надо отметить, что независимому тестированию могут быть подвергнуты не только компоненты, ответственные за визуализацию данных, но и редьюсеры, и создатели действий.

Redux — это замечательно, но используя эту технологию мы столкнулись с некоторыми трудностями.

Трудности при использовании Redux

▍Избыток шаблонного кода

Для того чтобы реализовать в приложении архитектуру Redux, приходится потратить немало времени, сталкиваясь при этом со всяческими странными понятиями и сущностями.

▍Хранилище Redux — это синглтон

В Redux хранилище данных построено с использованием паттерна «синглтон», хотя компоненты могут иметь несколько экземпляров. Чаще всего это не проблема, но в определённых ситуациях подобный подход к хранению данных может создавать некоторые сложности. Например, представим себе, что существуют два экземпляра некоего компонента. Когда в любом из этих экземпляров меняются данные, эти изменения сказываются и на другом экземпляре. В определённых случаях такое поведение может оказаться нежелательным, может понадобиться, чтобы каждый экземпляр компонента пользовался бы собственной копией данных.

Итоги

Вспомним наш главный вопрос, который заключается в том, стоит ли тратить время и силы на реализацию архитектуры Redux. Мы, в ответ на этот вопрос, говорим Redux «да». Эта архитектура помогает экономить время и силы при разработке и развитии приложений. Использование Redux облегчает жизнь программистов при необходимости частого внесения изменений в приложение, упрощает тестирование. Конечно, архитектура Redux предусматривает наличие немалого объёма шаблонного кода, но она способствует разбиению кода на модули, с которыми удобно работать. Каждый такой модуль может быть протестирован независимо от других, что содействует выявлению ошибок ещё на этапе разработки и позволяет обеспечить высокое качество программ.

Уважаемые читатели! Пользуетесь ли вы Redux в своих проектах?

Источник

Redux

Redux — это инструмент для управления состоянием данных и пользовательским интерфейсом в приложениях JavaScript с большим количеством сущностей. Представляет собой библиотеку JavaScript.

Название читается как «Редакс» и составлено из двух слов: reduce и flux. Reduce — это функция, которая приводит большую структуру данных к одному значению. Flux — архитектура приложения, при которой данные передаются в одну сторону. Инструмент основан на этих двух понятиях, поэтому они вынесены в название.

Обычно Redux используется в связке с фреймворками для JavaScript: React, TypeScript, Vue, Angular и другими. Реже он бывает нужен для написания кода на чистом JS. Имеет открытый исходный код и доступен бесплатно. Со всеми зависимостями весит всего около 2 Кб.

Для чего нужен Redux

Зачем нужен объект состояния

Состояние, или state, — это концепт, который обычно используют в сложных программах. Большое приложение содержит много данных, которые в любой момент могут потребоваться тому или иному компоненту.

Компоненты могут общаться напрямую, но в сложной программе реализовывать такой подход неудобно. Необходимо дописывать каждую функцию. Это загромождает код.

Поэтому требуется состояние — объект, который можно сравнить с диспетчерской. Он получает, хранит и при необходимости передает одним компонентам данные других. К нему можно обратиться, чтобы узнать, нажата ли кнопка, какое значение сейчас у переменной, выбрано ли условие. Это удобнее и проще, чем получать сведения непосредственно от компонента.

Что означает «управление состоянием»

Объектом состояния нужно управлять, иначе при работе с большими программами неизбежны ошибки. Например, функция может не узнать, что состояние обновилось, и продолжить работать со старым значением. Обновления происходят непредсказуемо, получит ли компонент верное значение — неизвестно.

Управление помогает сделать работу объекта логичной. Для этого существуют менеджеры состояния, или state managers. Их можно написать самому, но готовые решения удобнее и содержат больше полезных функций. Одно из таких решений — Redux.

Основные концепции

Redux базируется на трех принципах, из которых следует характер работы с ним.

Единый источник состояния. Все данные о состоянии приложения хранятся в одном месте, без копий. Глобальное состояние организовано как дерево объектов и называется state tree. Также используются термины «источник состояния» и «хранилище». Оба означают местонахождение глобального состояния. Единый источник нужен для централизации и отладки приложения. Работа становится проще, если данные находятся в одном месте.

Доступ к состоянию — только для чтения. Глобальное состояние заблокировано для записи. Компоненты приложения могут читать из него, но не переписывать по своему желанию. Это предотвращает непредсказуемые изменения. Ситуаций, когда объект изменился, а функции об этом не узнали, не бывает. При изменениях в состояние нужно отправить действие (action). Специальный объект сообщит о произошедшем.

Изменения — только через редукторы. Когда в состояние поступает действие, его обрабатывают редукторы, или редьюсеры (reducers). Это чистые функции — результат их выполнения зависит только от входных данных.

Редукторы берут объект состояния компонента, который изменился, и действие. На их основе они генерируют новый объект состояния. Функции всегда создают новый объект. Если действие сложное, а приложение большое, для него могут запуститься несколько редукторов.

Результат работы редуктора — новый объект состояния с актуальными данными, рассчитанными на основе информации из объекта-действия. Он попадает в дерево состояний вместо старого.

Как устроено приложение с Redux

Управление состоянием с помощью Redux можно разделить на три компонента:

Redux что это

Источник состояния существует в определенном виде. На его основе строится интерфейс приложения. Когда происходят изменения, в источник состояния поступает действие и редукторы перезаписывают состояние. Источник состояния изменяется.

Затем он отправляет новые данные всем компонентам, которые настроены их получать. Так реализована зависимость интерфейса от источника. Компоненты получают информацию и, если нужно, перерисовывают интерфейс в соответствии с ней.

Таким образом за состоянием изменяется интерфейс, так как он зависит от источника. Приложение становится реактивным, то есть мгновенно реагирующим на изменения. Количество ошибок уменьшается, а поведение программы становится более предсказуемым.

Преимущества Redux

Легкая работа с состоянием. Redux реализован просто и элегантно, мало весит и при этом эффективно управляет состоянием. Его основную функциональность можно уместить в десять строчек кода. Пользоваться Redux удобнее, чем самостоятельно писать менеджер состояний.

Упрощение масштабирования. Redux часто используют в связке с React, потому что в React не очень удобный встроенный алгоритм для управления состояниями. Его особенности приводят к тому, что приложение становится тяжело масштабировать. Redux решает эту проблему.

Простое изучение. Если разработчик уже знает один из основных фреймворков для JavaScript либо «чистый» JS, ему будет просто начать работу с Redux. Обучающие материалы и уроки находятся в открытом доступе.

Множество полезных инструментов. Для Redux существуют вспомогательные библиотеки и инструменты, которые сильно облегчают работу с приложениями. Компоненты модифицируют модель, описанную выше, или помогают работать с ней более гибко.

Как начать работать с Redux

Redux можно скачать и установить с помощью пакетного менеджера для JS-разработчиков: npm или yarn. Для этого достаточно команды в консоли:

После этого необходимо создать в директории с приложением отдельную папку для хранения файлов, связанных с Redux. Внутри нее можно размещать файлы для хранения источника состояний, файлы с кодом редукторов и другие. Структура должна быть понятной.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *