Плохие способы использования ngrx/effects

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

Плохие способы использования ngrx/effects

Оригинал статьи Документация ngrx Справочник RxJS

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

Ngrx/store — это библиотека Angular, которая помогает упаковывать сложность отдельных функций. Это происходит в том числе из-за использования в ngrx/store концепций функционального программирования, которые изолируют манипуляции с данными внутри функции. В хранилище (ngrx/store) редукторы (reducer), селекторы (select) и операторы RxJS являются чистыми функциями.

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

Побочные эффекты (side effects)

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

Было бы здорово просто отправлять действие (dispatch action) внутри компонента, когда пользователь отправляет форму, и обрабатывать побочный эффект в другом месте.

Библиотека эффектов(Ngrx/Effects) — это промежуточный слой(middleware) для обработки побочных эффектов в хранилище (ngrx/store). Она прослушивает отправленные действия в наблюдаемом (Observable) потоке, выполняет побочные эффекты, и возвращает в поток новые действия последовательным или асинхронным образом. Возвращенные действия также передаются в редуктор.

Возможность обрабатывать побочные эффекты RxJS-дружественным способом делает код чище. После отправки из компонента первого действия с типом SAVE_DATA вы создаете класс эффектов для обработки остальных действий:

Это упрощает работу компонента до отправки действий (dispatch actions) и подписки (subscribe) на наблюдаемые потоки (observables).

Библиотекой Ngrx/Effects легко злоупотребить

Ngrx/Effects — очень мощное решение, поэтому его легко использовать. Вот некоторые распространенные ошибочные способы использования библиотеки эффектов для хранилища состояний:

1. Дублирующее/производное состояние

Допустим, вы работаете над каким-то проигрывателем, и у вас есть следующие свойства в дереве состояний:

Поскольку аудио является типом мультимедиа, то вне зависимости от значения audioPlaying, значение mediaPlaying может иметь значение true. Как же убедиться, что mediaPlaying обновляется при обновлении audioPlaying?

Неверный ответ: используйте эффект!

Правильный ответ: если состояние mediaPlaying порождается другим состоянием, то это не истинное, а производное состояние. Оно должно быть в селекторе (select), а не в хранилище.

Теперь наше состояние может оставаться чистым и нормализованным, и мы не используем ngrx/Effects для чего-то, что не является побочным эффектом.

2. Связанные действия и редукторы

Допустим, у вас есть такие свойства в дереве состояний:

Когда пользователь удаляет элемент, то после завершения запроса на удаление, отправляется действие DELETE_ITEM_SUCCESS, чтобы обновить состояние нашего приложения. В редукторе запись удаляется из дерева. Но эта же запись в массиве FavoritesItems, будет ссылаться на удалённую запись в дереве. Как мы можем убедиться при отправке действия DELETE_ITEM_SUCCESS, что соответствующая запись удалена из FavoritesItems?

Неверный ответ: используйте эффект!

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

Правильный ответ: действие DELETE_ITEM_SUCCESS может обрабатываться и редуктором дерева записей, и редуктором массива записей FavoritesItems.

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

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

3. Выборка данных для компонента

Вашему компоненту нужны данные из хранилища, но данные должны быть сначала получены с сервера. Вопрос в том, как мы можем получить данные в хранилище, чтобы компонент мог их выбрать?

Болезненный ответ: используйте эффект!

В компоненте мы инициируем запрос, отправив действие:

В классе эффектов мы слушаем действие GET_USERS:

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

В классе эффектов мы слушаем оба действия:

Теперь другой разработчик добавляет компонент, которому требуется тот же HTTP-запрос (опускаем предположения о других компонентах). Компонент отправляет те же действия в тех же местах. Если оба компонента станут активны одновременно, то первый компонент для инициализации отправит HTTP-запрос. Когда второй компонент инициализируется (ngOnInit), он не будет отправлять запрос, потому что needUsers будет false. Отлично!

Когда первый компонент будет уничтожен, он отправит CANCEL_GET_USERS. Но второй компонент все еще нуждается в этих данных. Как мы можем предотвратить отмену запроса? Может быть, есть счетчик подписчиков (subscribe)? Я не буду беспокоиться об этом, но вы уловили суть. Мы начинаем надеяться, что есть лучший способ управления этими зависимостями данных.

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

Лучшее решение, которое я нашел для этого конкретного сценария, — это следующий пример в отличной статье. Там callApiY требует, чтобы callApiX уже был завершен. Тут убраны комментарии, чтобы код выглядел менее пугающим, но лучше прочитать оригинальный пост, чтобы познакомиться с деталями:

Теперь усложним задачу: сделаем так, что HTTP-запросы должны быть отменены, когда компоненты больше не активны.


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

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

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

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


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

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

Создадим наблюдаемые потоки в сервисе:

Подписки на users$ будут передаваться как для requireUsers$, так и для userSelectors.users$, но будут получать значения только от userSelectors.users$ (пример реализации muteFirst)

В компоненте:

Поскольку эта зависимость данных теперь просто наблюдаемый поток, мы можем подписаться и отписаться в шаблоне, используя асинхронные операторы в pipe(), и нам больше не нужно отправлять действия. Если приложение переходит на url/route другого компонента, подписанного на данные, HTTP-запрос отменяется, а веб-сокет закрывается.

Цепочки зависимостей данных можно обрабатывать так:

Сравним с предыдущим подходом:

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

Заключение

Ngrx/Effects — отличный инструмент! Однако необходимо ответить на следующие вопросы прежде чем принять решение о его использовании:

  • Это действительно побочный эффект?
  • Действительно ли ngrx/Effects — лучший способ справиться с этим?

Пожалуйста, поделитесь с нами своим мнением, особенно о любых ошибках. Моя следующая статья опирается на эту. Если я не допустил ошибок, она окажется ещё интереснее.

Хорошие способы использования ngrx/effects

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