Структура и подходы к организации экшенов, селекторов, редьюсеров и эффектов в Ngrx и Nx

Aleksandr Serenko
F.A.F.N.U.R
Published in
15 min readFeb 29, 2020
Angular + redux + Ngrx & Nx

В данной статье рассмотрим структуру Ngrx State, которую рекомендует Nx. Поговорим о создании экшенов, а также рассмотрим применение фасада для управления состоянием stat’а.

В одной из предыдущих статей, я рассматривал организацию state’а — Организация Stat’ов в Angular c Ngrx и Nx. В статье на примерах приведена структура, которая была рекомендована Nx. Но изменение подходов в Ngrx и Nx привело к созданию новой структуры, которая позволяет упростить store. В этой статье пересмотрим и доработаем принципы приведённые в статье, а также приведём несколько примеров для демонстрации описанных возможностей.

Если вы не знакомы с Redux, то сначала прочтите введение в Redux на официальном сайте — redux.js.org. Также для понимания процесса, вам необходимо понимание принципов работы Ngrx, с которыми можно ознакомиться на сайте — ngrx.io. Информацию о реализации Redux в Angular можно посмотреть в статье — Redux в Angular. Управление состояниями в Angular с помощью Ngrx и Nx.

Архитектура Ngrx

Более детально опишем принципы работы с Ngrx, прокомментировав основные моменты.

Если вы знакомы с Ngrx, то можете пропустить данный раздел и перейти к разделу — Структура state.

Обычно процесс создания state происходит следующим образом:

  1. Создание Action’ов
  2. Создание Reducer’а
  3. Создание Selector’ов
  4. Создание Effect’ов
  5. Создание Facade

Опишем все стадии создания state согласно списку.

Создание экшенов (Actions)

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

Action представляет собой простой интерфейс, который из обязательных полей имеет только type, который должен быть уникальным во всем проекте. Именно с помощью type, reducer понимает, какой action перед ним и что с ним нужно сделать.

Примером простого экшена может быть объект:

{
type: '[User] Load User'
}

Action может содержать необходимые данные, помимо типа. Например userId:

{
type: '[User] Load User',
userId: 55
}

Обычно, все дополнительные свойства называются payload, который формально объединён с action’ом.

Хорошей практикой считается вынесение всех данных в отдельный объект payload:

{
type: '[User] Load User',
payload: {
userId: 55
...
}
}

В Ngrx есть 2 пути создания экшенов:

  • Использование функции createAction
  • Использование класса (подход применяется в версии ngrx 7 или ниже)

Создание экшена с помощью createAction является рекомендуемым*.

Создание экшена с помощью класса выглядит следующим образом:

Как уже не сложно заметить, использование класса существенно увеличивает количество кода при одном action’е.

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

Если исходить из практики, то нужно отметить пару вещей:Важны правильные названия для Action’ов, так как халатное отношение к наименованию, может привести к замешательству разработчика и породить множество нежелательных side эффектов. Например использование грамматики в наименовании (LoadX, LoadingX, LoadedX) как правило не только мешает, но не даёт чёткого ответа на тип Action’а. Есть множество действий (неправильных глаголов), которые не имеют второй/третьей формы и поэтому, для описания подобного рода экшенов, приводит разработчика к придумыванию решений (костылей), которые могли бы отражать суть экшена. Подобный подход был представлен разработчиками Nx, хотя и возможно они просто использовали примеры Ngrx.

  • Важны правильные названия для Action’ов, так как посредственное отношение к наименованию, может привести к замешательству разработчика и породить множество нежелательных side эффектов. Например использование грамматики в наименовании (LoadX, LoadingX, LoadedX) как правило не только мешает, но не даёт чёткого ответа на тип Action’а. Есть множество действий (неправильных глаголов), которые не имеют второй/третьей формы и поэтому, для описания подобного рода экшенов, приводит разработчика к придумыванию решений (костылей), которые могли бы отражать суть экшена. Подобный подход был представлен разработчиками Nx, хотя и возможно они просто использовали примеры Ngrx.
  • Хоть и с первого взгляда, создание action’ов с помощью классов привлекает внимание сторонников ООП, практика показывает, что использование enum будет нужно только для создания action’а и его связи с reducer’ом. Однако, поддержка enum’а и порождение классов существенно увеличивают время и размер store, что как правило, не является оправданным.

Решение описанных проблем приведём в следующем разделе — Структура state.

Создание редьюсера (Reducer)

Reducer в Ngrx отвечает за переход state из одного состояния в другое, основываясь на типе action’а.

Reducer это pure функция, которая для одних и тех же параметров предоставит одно и тоже состояние. Также редьюсер является синхронной функцией (не реактивной) и не содержит реактивной логики. Вся реактивная логика в Ngrx обрабатывается с помощью Effect’ов, описание которых будет ниже.

Вся реактивная логика в Ngrx обрабатывается с помощью Effect’ов, описание которых будет ниже.

Обычно файл, в котором описывается редьюсер в Ngrx, содержит 3 вещи:

  • Feature key — ключ, который будет создан в root state, значением которого будет state.
  • State interface — интерфейс состояния разрабатываемого модуля.
  • Reducer — функция, реализующая редьюсер для конкретных action’ов данного модуля.

В качестве примера, приведём state для пользователя.

Feature key будет соответствовать названию модуля (user):

export const USER_FEATURE_KEY = 'user';

State для User, может включать в себя следующие параметры:

  • user — объект загруженного пользователя.
  • userApiError — ошибка, которая произошла при загрузке пользователя

Константа userInitialState — представляет собой начальное состояние UserState.

Reducer для пользователя можно представить так:

Так как нас интересует изменение state, в примере приведён action — loadUserSuccess, который просто получает User:

UserReducer при возникновении action’а — loadUserSuccess, записывает в UserState в свойство user, объект User’а, который был передан в loadUserSuccess.

При возникновении loadUserFailure, UserReducer запишет в UserState в userLoadError ошибку (error), которая была передана в loadUserFailure.

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

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

В данном случае, в конструкторе компонента генерируется новое событие — загрузка пользователя. Соответственно store, находит необходимый reducer, находит нужную мутацию (метод on c типом равным [User] Load User).

Так как в примере ожидается загрузка пользователя с помощью API, то это является уже реактивной логикой, которая будет обработана в эффекте (Effect).

Как упоминалось ранее, в ngrx есть несколько способов описания reducer’ов. Одним из способов, является использование функции createReducer, которая сейчас является предпочтительной.

Пример, приведённый выше, как раз использует createReducer.

Вторым способом является подход на использовании switch-case вместо функции createReducer и on.

Если сравнить реализации старого и нового подходов, то можно вновь отметить громоздкость описания в старом подходе.

Можно комбинировать новый формат action’ов со старым описанием reducer’ов. Для этого необходимо все типы action’ов вынести в enum.

Создание селекторов (Selectors)

Основное назначение селекторов — предоставить доступ к свойствам store. Селектор представляет pure функцию, которая выбирает из store нужные данны.

Так как store является реактивным и использовать селекторы можно только вместе с реактивностью. Простой пример использования селектора: this.store.pipe(select(UserSelectors.getUser))

Селекторы это единственная часть Ngrx, которая за долгое существование не потерпело существенных изменений.

Для создания селекторов необходимо использовать функцию — createSelector.

Отметим, что хотя и в официальной документации, связанной с selector’ами приведён пример из создания и использования, то может сложиться неверное предположение об использовании.

Это все справедливо, если вы не используете feature, и не разбиваете state, на множество маленьких обособленных state’ов.

Модель с использованием features работает следующим образом:

Сначала создаётся селектор, который формально выбирает из root state нужный state, соответствующий модулю:

export const getUserState = createFeatureSelector<UserState>(USER_FEATURE_KEY);

Затем, уже из выбранного state, выбирается нужные данные:

export const getUser= createSelector(getUserState, state => state.user);

Для примера с UserState, файл селекторов будет таким:

Создание эффектов (Effects)

Эффекты в Ngrx применяются для следующих целей:

  • Работа с асинхронными данными и функциями (например, загрузка данных по http, или создание websocket’ов)
  • Наблюдение за происходящими action’ами и порождение новых action’ов.

Эффекты не в ходят в ngrx/store модуль, являются отдельным пакетом, что подчёркивает отступление от стандартной модели Redux.

Создание эффекта представлено в двух вариантах:

  • использование функции createEffect
  • использование декоратора @Effect()

Простой эффект можно представить так:

Как можно увидеть из реализации, по типу action’а фильтруется событие и вызывается загрузка пользователя.

loadUser$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.loadUser),
mergeMap(action => this.userHttp.load(action.userId)
.pipe(
map(user => UserActions.loadUserSuccess({ user })),
catchError(error => UserActions.loadUserFailure({ error }))
))
)
);

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

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

loadUserSuccess$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.loadUserSuccess),
tap(() => {
console.log('User loaded!');
})
)
);

Стуруктура state глазами Nx

Ознакомившись с основами Ngrx можно поговорить о структуре state при разработке enterparise приложений с помощью монорепозитория Nx.

Если сгенерировать новый state с помощью Nx:

ng g @nrwl/angular:ngrx user --module=libs/users/src/lib/users-core/users-core.module.ts

то получим следующую структуру:

Схема nrwl/angular:ngrx генерирует следующие файлы для user state:

  • user.actions.ts — файл, в котором описаны все action’ы;
  • user.effects.ts — файл, который содержит все эффекты;
  • user.facade.ts — файл, реализующий facade над feature state;
  • user.model.ts — файл, который содержит абстракцию сущности, с которой поразумивается сохранение в state.
  • user.reducer.ts — файл, содержащий реализацию редьюсера и соответствующих мутаций;
  • user.selectors.ts — файл, включающий в себя все селекторы user state.

Рассмотрим более детально каждый сгенерированный файл.

Наименование Action’ов

Файл user.actions.ts:

Как можно увидеть, в файле создано 3 action’а:

  • loadUser
  • loadUserSuccess
  • loadUserFailure

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

Важным нюансом тут является наименование action’ов, где используется следующая парадигма:

Действие — Сущность — Состояние/Статус

Данная модель наименования объясняется следующим образом.

Допустим есть множество действий, которые необходимо совершить с сущностью: create, change, update, load, merge, delete, migrate, reset, confirm, …

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

Возьмём для примера 3 действия для user’а: create, delete, change.

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

Такая же проблема была и раньше в Nx и Ngrx. Если посмотреть примеры action’ов в Ngrx, то можно было увидеть нечто подобное:

Даже если абстрагироваться от стиля написания и переписать в упрощённом виде:

Данное зрелище не вызывает восторга или успокоения. В данном примере, всего 3 действия. Но если их будет больше 10 — будет полный мрак, и без рефакторинга уже точно не обойтись.

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

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

Опишем всю цепочку:

  • Инициировать событие, которые вызовет загрузку пользователя — loadUser
  • После успешной загрузки пользователя, вызвать событие сохранения загруженного пользователя в state — loadUserSuccess
  • Если при загрузке пользователя произошла ошибка, вызвать событие ошибки загрузки (для того, чтобы показать ошибку клиенту) — loadUserFailure.

Обычно, данной цепочки хватает, для реализации. Но чем сложнее логика, тем больше может становиться цепочка. Например, если вы используете SSR, и не хотите загружать предварительные данные, а оставить это на уровне browser.app. Тогда нужна ещё одна стадия — проверка возможности загрузки:

  • Вызов события, которое проверит возможность загрузки пользователя и вызовет событие загрузки — loadUser
  • Инициировать событие, которые вызовет загрузку пользователя — loadUserRun
  • После успешной загрузки пользователя, вызвать событие сохранения загруженного пользователя в state — loadUserSuccess
  • Если при загрузке пользователя произошла ошибка, вызвать событие ошибки загрузки (для того, чтобы показать ошибку клиенту) — loadUserFailure.

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

Для того, чтобы отлавливать подобные случаи, можно добавить состояние cancel:

  • Вызов события, которое проверит возможность загрузки пользователя и вызовет событие загрузки — loadUser, если загрузка не возможно вызвать событие loadUserCancel
  • Инициировать событие, которые вызовет загрузку пользователя — loadUserRun
  • После успешной загрузки пользователя, вызвать событие сохранения загруженного пользователя в state — loadUserSuccess
  • Если при загрузке пользователя произошла ошибка, вызвать событие ошибки загрузки (для того, чтобы показать ошибку клиенту) — loadUserFailure.

Приведём реализацию:

Как можно заметить, мы однозначно можем выстроить цепочку, так как все action’ы начинаются одинаково с loadUser, а все состояния различны. Это очень удобно при использовании IDE, где при авто подсказках, все ваши action’ы выстроены друг под другом.

Вынесение данных из action’а в payload

Последним, что можно сказать об организации action’ов, это правила передачи параметров в action.

Так как createAction, формально создаёт новый объект со свойством type и всеми остальными свойствами, переданными в props, то получается непреднамеренное объединение двух разных составляющих Redux.

Рассмотрим проблему на примере.

Допустим необходима загрузка пользователя по id. Для этого в action добавим параметр.

Однако, результатом работы будет action вида:

И в этом нет ничего плохого. Но если вы решите передавать payload между action’ами, то тогда, будет необходимо дублировать и указывать все переданные параметры. В нашем случае это один параметр:

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

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

Тогда, согласно описанному подходу, action’ы для пользователя будут выглядеть так:

где ActionPropsPayload простая абстракция для создания объекта со свойством payload.

Организация Reducer’а

Шаблон редьюсера:

Из примера видно, что Nx запихнули в schematic’у использование EntityState.

Ngrx/Entity — это отдельная песня, про улучшение и автоматизацию работы с сущностями.

Как показывает практика, если вы пишите проект с нуля, где в backend есть порядок и согласованность, а все методы действительно максимально идеализированы и соответствуют RestAPI, то тогда использование ngrx entity оправдано. Но очень часто это не так ..

Вы можете ознакомиться с принципами Entity на сайте ngrx и попробовать использовать данные подходы, но в данной статье мы ограничимся стандартным ядром ngrx.

Поэтому удалим все использования entity, а также немного переименуем названия, где State -> UserState.

Наименования свойств в State также важно, как задание нужных имён для action’ов.

Для задания имён свойствам предлагается давать максимально приближенные к названиям action’ов. Например для загрузки пользователя это будет:

Единственным отступлением, является свойство user. Мы не стали добавлять постфикс load, так как это чрезмерно и часто не нужно, для контроля.

Также хорошей практикой считается переименование intialState в именованную переменную, связанную со state, где в нашем случае это userInitialState.

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

И соответственно сами мутации, которые происходят при вызове того или иного action’а:

Выше, мы описывали принцип работы экшенов, в виде цепочки событий. В данном примере приведена её реализация.

  • UserLoad вызывает события проверки возможности загрузки. Данный action не изменяет состояние state.
  • Если загрузка пользователя возможна, вызывается событие UserLoadRun, которое записывает в state, состояние, что происходит асинхронная загрузка пользователя, а также сбрасывается предыдущая ошибка загрузки, если таковая была.
  • При успешной загрузке вызывается событие UserLoadSuccess, где в state записывается загруженный User и флаг асинхронной загрузки ставится false.
  • При неуспешной загрузке вызывается событие UserLoadFailure, где в state записывается ошибка загрузки и флаг асинхронной загрузкт ставится false.

В итоге, полный файл user.reducer.ts получит вид:

Реализация селекторов

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

Приведём конкретный пример селекторов для User State:

Реализация Effect’ов и использование DataPersistence

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

Посмотрим, что предоставляет нам Nx, для упрощения разработки эффектов.

Сгенерированный файл эффектов содержит в себе:

Основной особенностью является оператор fetch. Как видно из примера, fetch порождает 2 action’а: loadUserSuccess и loadUserFailure при успехе или неуспехе соответственно.

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

Так как статья является частью серии статей про Angular, то в статье используются ранее реализованное GraphQL API. Примеры конкретной реализации, можете посмотреть в репозитории.

Добавив загрузку, получим следующие 2 эффекта.

  • loadUser$ — Эффект который наблюдает за процессом начала загрузки пользователя. Если происходит событие loadUser, эффект проверят возможность загрузки и запускает загрузку пользователя, генерируя action loadUserRun.
  • loadUserRun$ — эффект, который непосредственно запускает реактивную загрузку пользователя. И при окончании загрзуки испускает action либо успешной загрузки, либо неуспешной.

force в экшене — это признак принудительной загрузки.

Как можно увидеть в первом эффекте:

Для того, чтобы получить root state (store), приходиться использовать оператор withLatestFrom. В Nx посчитали, что так писать достаточно громоздко, и реализовали сервис — DataPersistence.

DataPersistence — реализует 3 популярные стратегии:

  • Fetch — обычный запрос, аналогичный выше описанному
  • Optimistic Updates — запрос, который формально не дожидается ответа от сервера и считает, что запрос прошёл успешно.
  • Pessimistic Updates — запрос, который сначала обновляет данные на сервере, а уже после обновляет состояние клиента.

Более подробно о DataPersistence, ознакомитесь в документации.

Пример с использованием DataPersistence:

Утилиты для работы с Effect’ами

Так как большинство работы в effect’ах однообразно, то можно сгруппировать основные методы и вынести в абстрактный класс:

Расширяя UserEffects, получим:

  • getState — утилита, которая из root state по feature key возвращает partial state
  • isBrowser — обертка над isPlatformBrowser(this.platformId)
  • errorHandler — утилита, которая позволяет генерировать новый action с ошибкой, а также выводить ошибку в консоль

Создание Facade

Заключающим моментом является создание facade. Формльно фасад представляет обёртку над store и скрывает всю реализацию (dispatch, select) store.

Подробнее с принипом фасадов можно ознакомиться в статье — NgRx + Facades: Better State Management.

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

В нашем случае фасад для User State будет следующим:

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

user$ = this.store.pipe(select(UserSelectors.getUser));

userLoadError$ = this.store.pipe(select(UserSelectors.getUserLoadError));

userLoadRun$ = this.store.pipe(select(UserSelectors.getUserLoadRun));

Также, для всех цепочек событий, мы написали инициаторы данных событий.

loadUser(force?: boolean): void {
this.store.dispatch(UserActions.loadUser({ payload: { force } }));
}

Так как весь Angular построен на абстракциях, а Facade это сервис, создадим абстракцию и для фасада:

И добавим его в реализацию фасада:

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

Запустим в нашем демо проекте:

yarn run backend:db:start // запуск сервера баз данных
yarn run serve:backend:api // запуск бекенд приложения с grpahql
yarn run redux:serve // запуск angular приложения

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

Откроем приложение в браузере:

Попытаемся загрузить пользователя, без авторизации:

Как видно на скриншоте, произошло 3 action’а:

  • LoadUser — нажатие на кнопку загрузить текущего пользователя
  • LoadUserRun — загрузка пользователя с помощью API
  • LoadUserFailure — ошибка загрузки пользователя

Если открыть ответ, то можно увидеть 401 статус:

Нажмём на кнопку авторизации, где произойдёт установка логина и пароля:

Извините формочку ввода не делал, так как это не часть данной статьи.

Видим, что произошло ещё 4 action’а:

  • SignInSet — установка логина и пароля для запроса
  • SignIn — запуск авторизации
  • SignInRun — запрос на авторизацию по API
  • SignInSuccess — успешная авторизация

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

Если открыть вкладку network, то можно увидеть ответ сервера:

Авторизация вернула JWT access token. Теперь можем запросить текущего пользователя:

Видим снова 3 action’а, но только теперь загрузка прошла успешно.

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

И чтобы развеять сомнения, что я отлично пользуюсь Photoshop (или конечно gimp, так как на unix есть проблемы с запуском photoshop), небольшая демонстрация работы целиком:

На скриншотах выше было проделано тоже самое.

Резюме

В данной статье рассмотрели два основных блока:

  • Принципы создания state с помощью Ngrx
  • Принципы организации state’ов в Ngrx и Nx.

В первом блоке детально разобрали создание следующих сущностей:

  • Рассмотрели создание action’ов, поговорили о старом и новом подходах, а также обсудили важность наименования action’ов, привели примеры создания экшенов;
  • Рассмотрели создание редьюсера, поговорили о основных компонентах редьюсера, привели примеры создания редьюсера старым и новым подходами;
  • Рассмотрели создание селекторов, привели примеры создания селекторов
  • Рассмотрели создание эффектов, поговорили об основных назначениях эффектов, привели примеры создания.

Во-втором блоке поговорили о общих подходах создания Ngrx State.

По частям разобрали и прокомментировали создания сущностей:

  • Рассмотрели примеры наименования экшенов, а также привели рекомендации по созданию action’ов;
  • Рассмотрели примеры организации State’а, а также предоставили комментарии по созданию мутаций;
  • Рассмотрели примеры организации эффектов в Ngrx. Поговорили о DataPersistence и привели несколько примеров для упрощения создания и управления событиями;
  • Рассмотрели примеры создания facade. Поговорили о важности применения фасадов и привели примеры использования.

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

Исходники

Все исходники находятся на github, в репозитории:

Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий тег — nx-state.

git checkout nx-state

Код можно посмотреть в разделе https://github.com/Fafnur/medium-stories/tree/master/libs/users

Для демо проекта, использовалось приложение из предыдущей статьи — redux — https://github.com/Fafnur/medium-stories/blob/master/apps/frontend/redux

Ссылки

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

Medium: https://medium.com/fafnur
Добавляйтесь в группу ВК: https://vk.com/fafnur
Добавляйтесь в группу в Fb: https://www.facebook.com/groups/fafnur/
Телеграм канал: https://t.me/f_a_f_n_u_r
Twitter: https://twitter.com/Fafnur1
Instagram: https://www.instagram.com/fafnur

Предыдущие статьи:

  1. Angular 9, Universal и Nx. Новые правила сборки SSR приложения.
  2. Кроссплатформенные web storage в Angular 9. Реализация LocalStorage, SessionStorage и Cookies в Angular Universal.
  3. Мультиязычность ngx-translate в Angular 9 c монорепозиторием Nx.
  4. Разбиение локализации ngx-translate на несколько файлов в Nx
  5. Бесконечный скролл в Angular 9 с помощью Intersection Observer API
  6. Redux в Angular. Управление состояниями в Angular с помощью Ngrx и Nx.

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock