Redux ducks что это
[Redux] Мой любимый устаревший вопрос на собеседовании
Время от времени мне приходится проводить собеседования. И сегодня я хочу поделиться моими любимыми вопросами на тему Redux. К сожалению, вопросы уже устарели, т.к. они касаются компонента высшего порядка connect, который активно заменяют на хуки. Но connect может уже и не сильно актуален, а принципы на которых он построен абсолютно не изменились (Данная статья является расшифровкой видео).
И так давайте перейдем к самим вопросам
После общего вопроса: “Что такое Redux?”. Я обычно спрашивал: “Какой первый параметр принимает connect?”. Тут все отвечают правильно: “mapStateToProps”. Но вот на вопрос, а что такое «mapStateToProps», какой это тип данных. Некоторые уже начинают отвечать неправильно.
Главный вопрос
Хорошо mapStateToProps это функция. Тогда давайте представим следующую ситуацию. Допустим у нас на странице несколько независимых блоков. Например один блок это список пользователей, другой блок список машин, третий блок это список квартир и так далее. В итоге на странице несколько абсолютно независимых блоков. Каждый блок обернут в свой connect и тянет только информацию своего блока.
Дальше, допустим, мы решили нажать кнопку удаления пользователя, которая обновляет store и удаляет из него пользователя.
И так, главный вопрос: «сколько разных функций mapStateToProps при этом вызовется?» Добавлю даже варианты ответа:
Осторожно ниже ответ!
И правильный ответ.
Номер один. На этот вопрос многие дают ответ номер два.
По их мнению mapStateToProps вызывается только у компонента, контент которого обновился. И это звучит на первый взгляд очень логично. Но дальше можно задать уточняющий вопрос: “А как тогда redux понимает, какой именно нужно вызвать mapStateToProps?”. И этот вопрос, чаще всего, заставляет разработчиков задуматься, о том, что они возможно неправильно ответили на предыдущий вопрос.
Давайте вспомним как выглядит функция mapStateToProps.
Она принимает в качестве параметра переменную state, в которой лежит состояние всего стора. И только в этой функции мы определяем какие именно данные нужно передать в компонент. Это значит, что до вызова функции mapStateToProps, redux понятия не имеет, нужно рендерить компонент обернутый в connect или не нужно.
Если визуализировать картину это будет выглядеть следующим образом:
Action обновляет store. Store в свою очередь вызывает все зарегистрированные функции mapStateToProps, которые передают нужные компонентам данные в connect. И далее лишь один connect заставит обновиться компонент пользователей. Вот так работает наш пример.
Как это работает под капотом
Параметры mapStateToProps
Хорошо с mapStateToProps стало более менее понятно, как это работает. Но теперь хочется понять, как именно connect понимает нужно ему обновлять компонент, который он оборачивает или нет. Для того чтобы в этом разобраться мы изучим как работает connect под капотом.
Но перед этим, еще один мини вопрос (это для тех кто мнит себя знатаком инструментов). Мы знаем, что connect принимает несколько параметров, таких как mapStateToProps, mapDispatchToProps. Вопрос, сколько параметров можно передать в connect? Оборачиваемый компонент не считается за параметр. И так варианты ответа: 2? 3? 4? или 5?
И правильный ответ connect принимает целых 4 параметра. Давайте посмотрим исходники и убедимся в этом (ссылка на исходники).
Первые два нам хорошо известны. Далее менее популярный mergeProps, который позволяет нам сгруппировать данные полученные из первых двух функций.
И практически не используемый никем 4-ый параметр. И он представляет из себя объект, через который можно донастроить работу вашего connect.
Из интересных в нем настроек, я хотел бы обратить внимание на эту 4-ку.
Ядро connect-а
Проходиться по всему коду connect займет много времени, поэтому я перейду сразу к интересному месту. А именно, где эти функции вызываются. Есть одна фабрика, которая возвращает такую не хитрую функцию как pureFinalPropsSelector (ссылка на исходники).
Посмотрим для начала функцию, которая вызывается на первом рендере.
Функция, вычисляет значения mapStateToProps, mapDispatchToProps, mergeProps переключает флаг hasRunAtLeastOnce на true и на этом первый рендер окончен. Возвращает эта функция mergedProps, который мы и получаем в нашем компоненте.
С другой стороны функция, которая вызывается на второй и последующие рендеры, она более интересная.
Сначала она сравнивает не изменились ли props.
Потом проверяет не изменился ли сам redux store.
И в зависимости от того что именно изменилось, уже решает, какую именно функцию вызывать. Самый популярный случай, это все же изменения именно store. Поэтому мы рассмотрим функцию handleNewState.
Код достаточно простой. Вычисляется новый результат выполнения функции mapStateToProps и далее с помощью той самой функции areStatePropsEqual сравнивается с предыдущим значением. И если statePropsChanged равно true, тогда мержатся все props в один объект и отдаются обернутому компоненту.
Чтобы вы не пугались функции mergeProps. Если мы не передаем ее третьим параметром, в этом случае по дефолту она выглядит вот так:
Согласитесь, крайне просто.
Подытожить исследования исходников connect можно мыслью, что вся магия держится на одном простом сравнении результата выполнения mapStateToProps с помощью shallowEqual. Но, надо обязательно помнить, что shallowEqual имеет свои ограничения.
Как не стоит делать
Давайте рассмотрим несколько неудачных примеров:
На первый взгляд этот код выглядит вполне себе жизнеспособным. Пользователю на экране мы хотим показать имя состоящее из нескольких свойств.
Так делать не рекомендуется. Вспомним о том как работает shallowEqual. В нашем случае, на каждый вызов mapStateToProps, мы возвращаем новую ссылку на юзера, т.к. мы создаем новый объект каждый раз. Проблема именно в этих фигурных скобках.
В результате компонент, который мы обернули в connect будет рендериться в 100% случаев, когда обновляется store и даже, если обновился не user, а какие-нибудь другие данные.
Вся загвоздка в методе filter, который всегда возвращает новый массив, а не мутирует предыдущий. Таким образом shallowEqual всегда будет возвращать false.
И соответственно, если ранее рендерился только компонент, у которого обновились используемые им данные, то при таком написании кода, абсолютно все компоненты начнут рендериться, даже если обновились данные, которые они никогда и не используют. Как вы понимаете, это плохо скажется на перфомансе вашего проекта.
Решить эту проблему можно разными путями. Но один из новых путей, с которыми мы сегодня познакомились, вероятно не самый эффективный, это передать в текущий connect 4-ым параметром функцию areStatePropsEqual, где мы сами опишем как лучше сравнивать значения между рендерами.
Если же вы не поняли, в чем именно кроется проблема, я бы рекомендовал более детально изучить тему “передача параметров по ссылке и по значению в javascript” и после погуглить «как работает метод shallowEqual». И когда вы усвоите материал вернуться к этой статье и возможно для вас откроется много нового.
Дисклеймер
В этой статье я хотел поделиться с вами тем, как работает connect под капотом. Все слова про собеседования это действительно правда, но не основная тема этой статьи.
Redux ducks что это
Ducks: Redux Reducer Bundles
I find as I am building my redux app, one piece of functionality at a time, I keep needing to add
To me, it makes more sense for these pieces to be bundled together in an isolated module that is self contained, and can even be packaged easily into a library.
These same guidelines are recommended for
Java has jars and beans. Ruby has gems. I suggest we call these reducer bundles «ducks», as in the last syllable of «redux».
There will be some times when you want to export something other than an action creator. That’s okay, too. The rules don’t say that you can only export action creators. When that happens, you’ll just have to enumerate the action creators that you want. Not a big deal.
There are configurable BattleCry generators ready to be downloaded and help scaffolding ducks:
The migration to this code structure was painless, and I foresee it reducing much future development misery.
Although it’s completely feasable to implement it without any extra library, there are some tools that might help you:
Please submit any feedback via an issue or a tweet to @erikras. It will be much appreciated.
Масштабирование Redux-приложения с помощью ducks
В преддверии старта курса «React.js разработчик» подготовили перевод полезного материала.
Как масштабируется front-end вашего приложения? Как сделать так, чтобы ваш код можно было поддерживать полгода спустя?
В 2015 году Redux штурмом взял мир front-end разработки и зарекомендовал себя как стандарт выйдя за рамки React.
В компании, в которой я работаю, недавно закончился рефакторинг большой кодовой базы на React, где мы внедрили redux вместо reflux.
Нам пришлось пойти на этот шаг, потому что движение вперед оказалось невозможным без хорошо структурированного приложения и четкого набора правил.
Кодовой базе уже больше двух лет и reflux был в ней с самого начала. Нам пришлось менять код, сильно завязанный на компонентах React, который никто не трогал больше года.
Опираясь опыт от проделанной работы, я создал этот репозиторий, который поможет объяснить наш подход к организации кода на redux.
Когда вы узнаете больше о redux, actions и reducers, вы начинаете с простых примеров. Множество туториалов, доступных на сегодняшний день, дальше них не заходят. Однако если вы создаете на Redux что-то сложнее, чем список задач, вам понадобится разумный способ масштабирования вашей кодовой базы с течением времени.
Кто-то однажды сказал, что в computer science нет задачи сложнее, чем давать разным вещам названия. Я не мог не согласиться. В таком случае структурирование папок и организация файлов будут стоять на втором месте.
Давайте посмотрим на то, как мы раньше подходили к организации кода.
Function vs Feature
Есть два общепринятых подхода к организации приложений: function-first и feature-first.
На скриншоте слева структура папок организована по принципу function-first, а справа — feature-first.
Function-first означает, что ваши каталоги верхнего уровня называются в соответствии с файлами внутри. Итак, у вас есть: containers, components, actions, reducers и т.д.
Такое вообще не масштабируется. По мере роста вашего приложения и появления нового функционала, вы будете добавлять файлы в те же папки. В итоге, вам нужно будет долго скролить содержимое одной из папок, чтобы найти нужный файл.
Еще одна проблема заключается в объединении папок. Один из потоков вашего приложения, вероятно, потребует доступ к файлам из всех папок.
Одним из преимуществ этого подхода является то, что он умеет изолировать, в нашем случае React от Redux. Поэтому если вы захотите изменить библиотеку управления состоянием, вы будете знать, какие папки вам понадобятся. Если вам понадобится менять библиотеку view, вы сможете оставить нетронутыми папки с redux.
Feature-first значит, что каталоги верхнего уровня будут называться в соответствии с основным функционалом приложения: product, cart, session.
Такой подход гораздо лучше масштабируется, поскольку каждая новая фича лежит в новой папке. Однако у вас нет разделения между компонентами Redux и React. Изменение в одном из них в долгосрочной перспективе – задача непростая.
Помимо этого, у вас будут файлы, которые не будут относиться ни к одной функции. В итоге все сведется к папке common или shared, поскольку вам же захочется использовать свой код в разных фичах вашего приложения.
Объединяя лучшее двух миров
Хоть это и не относится к теме статьи, хочу сказать, что файлы управления состоянием от файлов UI нужно хранить отдельно.
Думайте о своем приложении в долгосрочной перспективе. Представьте себе, что произойдет с вашим кодом, если вы перейдете с React на что-то иное. Или подумайте о том, как ваша кодовая база будет использовать ReactNative параллельно с веб-версией.
В основе нашего подхода лежит принцип изоляции кода React в одной папке, которая называется views, а кода redux в другой папке, которая называется redux.
Такое разделение на начальном уровне дает нам гибкость организовывать отдельные части приложения совершенно разными способами.
Внутри папки views мы поддерживаем подход к организации файлов function-first. В контексте React это выглядит естественно: pages, layouts, components, enhancers и т.д.
Чтобы не сходить с ума от количества файлов в папке, внутри этих папок можно использовать подход feature-first.
Тем временем в папке redux…
Вводим re-ducks
Каждая функция приложения должна соответствовать отдельным actions и reducers, чтобы был смысл применять подход feature-first.
Оригинальный модульный подход ducks хорошо упрощает работу с redux и предлагает структурированный способ добавления нового функционала в ваше приложение.
Тем не менее, вы хотели понять, что происходит при масштабировании приложения. Мы осознали, что способ организации по одному файлу на фичу загромождает приложение и делает его поддержку проблемной.
Так появился re-ducks. Решение состояло в разделении функционала на duck-папки.
Если вы хотите убедиться, что абстракции – это плохо, посмотрите этой видео с Cheng Lou.
Давайте рассмотрим содержание каждого файла.
Types
Файл types содержит имена actions, которые вы выполняете в своем приложении. В качестве хорошей практики, вы должны попытаться охватить область имен, соответствующую функции, которой они принадлежат. Такой подход поможет при дебаге сложных приложений.
Actions
В этом файле содержатся все функции action creator.
Обратите внимание, что все actions представлены функциями, даже если они не параметризованы. Последовательный подход является наиболее приоритетным для большой кодовой базы.
Operations
Для представления цепных операций (operations) вам понадобится redux middleware, чтобы улучшить функцию dispatch. Популярные примеры: redux-thunk, redux-saga или redux-observable.
В нашем случае используется redux-thunk. Нам нужно отделить thunks от action creators даже ценой написания лишнего кода. Поэтому мы будем определять операцию как обертку над actions.
Если операция отправляет только один action, то есть фактически не использует redux-thunk, мы пересылаем функцию action creator. Если операция использует thunk, она может отправить много actions и связать их с помощью promises.
Зовите их операциями, thunks, сагами, эпиками, как захотите. Просто обозначьте для себя принципы наименования и придерживайтесь их.
В самом конце мы поговорим про index и увидим, что операции – это часть публичного интерфейса duck. Actions инкапсулируются, операции становятся доступными извне.
Reducers
Если у вас более многогранная функция, вам определенно стоит использовать несколько reducer’ов для обработки сложных структур состояний. Помимо этого, не бойтесь использовать столько combineReducer’ов, сколько нужно. Это позволит свободнее работать со структурами объектов состояний.
В большом приложении дерево состояний будет состоять минимум из трех уровней. Функции reducer должны быть как можно меньше и обрабатывать только простые конструкции данных. Функция combineReducers – это все, что вам нужно для создания гибкой и поддерживаемой структуры состояния.
Selectors
Наряду с операциями, селекторы (selector) являются частью публичного интерфейса duck. Разница между операциями и селекторами схожа с паттерном CQRS.
Селекторные функции берут срез состояния приложения и возвращают на его основе некоторые данные. Они никогда не вносят изменения в состояние приложения.
Index
Этот файл указывает на то, что будет экспортироваться из duck-папки.
Он:
Tests
Преимущество использования Redux вместе со структурой ducks состоит в том, что вы можете писать тесты прямо после кода, который нужно протестировать.
Тестирование вашего кода на Redux довольно прямолинейно:
Внутри этого файла вы можете писать тесты для reducer’ов, операций, селекторов и т.д.
Я мог бы написать целую отдельную статью о преимуществах тестирования кода, но их уже и так достаточно, поэтому просто тестируйте свой код!
Вот и все
Приятная новость о re-ducks состоит в том, что вы можете использовать один и тот же шаблон для всего своего кода redux.
Подход к разделению на основе feature для вашего кода redux помогает вашему приложению оставаться гибким и масштабируемым по мере роста. А подход к разделению на основе function будет хорошо работать, при построении маленьких компонентов, которые являются общими для разных частей приложения.
Вы можете взглянуть на полноценную кодовую базу react-redux-example здесь. Имейте ввиду, что в репозитории идет активная работа.
Как вы организуете свои redux-приложения? Я с нетерпением жду отзывов об описанном подходе.
Краткое руководство по Redux для начинающих
Авторизуйтесь
Краткое руководство по Redux для начинающих
Библиотека Redux — это способ управления состоянием приложения. Она основана на нескольких концепциях, изучив которые, можно с лёгкостью решать проблемы с состоянием. Вы узнаете о них далее, в этом руководстве по Redux для начинающих.
Примечание Вы читаете улучшенную версию некогда выпущенной нами статьи.
Содержание:
Когда нужно пользоваться Redux?
Redux идеально использовать в средних и крупных приложениях. Им стоит пользоваться только в случаях, когда невозможно управлять состоянием приложения с помощью стандартного менеджера состояний в React или любой другой библиотеке.
Простым приложениям Redux не нужен.
Использование Redux
Разберём основные концепции библиотеки Redux, которые нужно понимать начинающим.
Неизменяемое дерево состояний
В Redux общее состояние приложения представлено одним объектом JavaScript — state (состояние) или state tree (дерево состояний). Неизменяемое дерево состояний доступно только для чтения, изменить ничего напрямую нельзя. Изменения возможны только при отправке action (действия).
Действия
Действие (action) — это JavaScript-объект, который лаконично описывает суть изменения:
Типы действий должны быть константами
В простом приложении тип действия задаётся строкой. По мере разрастания функциональности приложения лучше переходить на константы:
и выносить действия в отдельные файлы. А затем их импортировать:
Генераторы действий
Генераторы действий (actions creators) — это функции, создающие действия.
Обычно инициируются вместе с функцией отправки действия:
Или при определении этой функции:
Редукторы
При запуске действия обязательно что-то происходит и состояние приложения изменяется. Это работа редукторов.
Что такое редуктор
Редуктор (reducer) — это чистая функция, которая вычисляет следующее состояние дерева на основании его предыдущего состояния и применяемого действия.
Чистая функция работает независимо от состояния программы и выдаёт выходное значение, принимая входное и не меняя ничего в нём и в остальной программе. Получается, что редуктор возвращает совершенно новый объект дерева состояний, которым заменяется предыдущий.
Чего не должен делать редуктор
Редуктор — это всегда чистая функция, поэтому он не должен:
Поскольку состояние в сложных приложениях может сильно разрастаться, к каждому действию применяется не один, а сразу несколько редукторов.
Симулятор редуктора
Упрощённо базовую структуру Redux можно представить так:
Состояние
Список действий
Редуктор для каждой части состояния
Редуктор для общего состояния
Хранилище
Хранилище (store) — это объект, который:
Хранилище в приложении всегда уникально. Так создаётся хранилище для приложения listManager:
Хранилище можно инициировать через серверные данные:
Функции хранилища
Прослушивание изменений состояния:
Поток данных
Поток данных в Redux всегда однонаправлен.
Передача действий с потоками данных происходит через вызов метода dispatch() в хранилище. Само хранилище передаёт действия редуктору и генерирует следующее состояние, а затем обновляет состояние и уведомляет об этом всех слушателей.
Советуем начинающим в Redux прочитать нашу статью о других способах передачи данных.
Тестирование redux
На примере обычного блога (получение из API данных для post-comments), продемонстрирую, как покрываю тестами redux-слой. Исходники доступны тут.
Вместо разделенных actions и reducers, применяю ducks-pattern, который сильно упрощает как разработку, так и тестирование redux-а в приложении. А ещё использую крайне полезный инструмент — redux-act, но важно в поле description метода createAction() добавлять исключительно: цифры, заглавные буквы и подчеркивания (proof).
Для начала тест для простого «action creator» типа < type, payload >— app.setLoading():
Минимум для первого запуска теста:
Копирую из консоли значение для expectedActions:
Применяю actions (с данными в payload для каждого action) к рутовому редюсеру, полученному из combineReducers():
Следует пояснить, что store создается с функцией обратного вызова mockStore(() => state) — чтобы обеспечить текущее состояние при вызовах getState() внутри сайд-эффектов redux-thunk.
Вот и всё, первый тест готов!
Далее интереснее, нужно покрыть тестами сайд-эффект post.load():
Хотя comments.load() тоже экспортируется, но тестировать его отдельно не имеет особого смысла, т.к. он используется только внутри нашего post.load():
Не знаю, как сделать лучше, но ради инициализации редюсера router, пришлось пересобрать рутовый редюсер в reducerMock. Плюс обманки для двух запросов к axios. Ещё к store.dispatch() добавился return, т.к. обернуто в Promise; но есть альтернатива — функция обратного вызова done():
А в остальном тест для сайд-эффекта не сложнее теста для простого «action creator». Исходники доступны тут.