Redux в Angular. Рекомендации по организации state.

Aleksandr Serenko
F.A.F.N.U.R
Published in
6 min readNov 24, 2022

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

Храните в state только данные

При начале использования Ngrx всегда непонятно, что должно храниться в state.

В определенный момент появляется желание абсолютно все хранить в state.

Например, для новости это может быть: загружается ли новость, ошибка о загрузке новости, процесс прочтения новости, процесс удаления новости, процесс изменения новости (loading, reading, removing, chaning, error).

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

Разберем пример создания новости. Можно добавить флаг создания в state и сделать так, что теперь новость может создаваться только одна в один момент времени. Это приводит к тому, что теперь при создании новости нужно проверять, а не создается ли другая новость. Если новость создается нужно ждать пока новость не создаться и только после этого добавлять другую новость.

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

Что делать если флаг нужен, но не хранить его в state?

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

Если таких компонентов несколько то тогда можно подумать о создании свойства в state. Однако тут нужно учесть тот случай, что данные разные и нужно уметь правильно реагировать на ошибки. Если есть два компонента, которые параллельно создаются, то соответственно они имеют два различных состояния creating. И один компонент может успешно завершиться, а другой упасть с ошибкой. Все это нужно учесть в state. И тут либо нужно создавать какую-то логику в контроле создаваемых сущностей в стейте, либо просто для каждого из компонентов создать собственное свойство creating. Возможно это будет дублирование логики, но с другой стороны вы существенно упростите реализацию, вследствии чего повысите надежность кода. А так как Angular мощный фреймворк, то вы можете создать сервис с данным состоянием и подпиской на соответствующие экшены, а потом подключить этот сервис в компоненте. Это позволит устранить дублирование кода.

Не храните ошибки в state

Еще одним советом будет указание не хранить ошибки в state. Если вы храните ошибки в хранилище, то необходим механизм сбрасывания ошибок.

Это можно объяснить на примере авторизации.

Допустим есть страница с авторизацией. Клиент вводит невалидные данные и получает 400ю ошибку с сообщением: “Указанные логин и пароль не найдены”. Если ошибку сохранить в стейте и потом ее вывести в шаблоне, то клиент при вводе неверных данных увидит ошибку. Однако, если этот клиент уйдет со страницы авторизации на главную, а потом вернется на страницу авторизации, то клиент увидит ошибку из стейта, так как ошибка была получена в прошлый раз и сохранена в стейте.

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

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

Количество подписок не увеличивается, так как нужно подписаться на loginError$ или loginFailure$.

Не используйте redux, если вы не храните полученные данные

Допустим необходимо реализовать сервис синхронизации. Его основное назначение дергать определенный endpoint через заданное время.

Кажется, что можно создать стейт, положить туда последние время синхронизации, добавить 3 экшена: sync, syncSuccess и syncFailure, добавить один эффект sync$ где будет interval, который будет опрашивать сервер.

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

  • создать экшены;
  • создать редьюсер;
  • создать эффекты;
  • создать апи сервис;
  • создать модуль и подключить туда feature store.

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

В итоге для задачи, где нужно опрашивать один endpoint вы получаете сложную структуру из объектов, классов и сервисов.

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

Не изменяйте вложенные объекты в объектах

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

interface Post { 
id: number;
title: string;
owner: {
id: number;
name: string;
}
}

Если изменять свойства у owner, то ngrx не сможет отследить эти изменения, так как сам объект не меняется.

Для того чтобы были обнаружены изменения нужно явно изменить owner:

const post: Post = { 
id: 1,
title: 'Title',
owner: {
id: 1,
name: 'user'
}
};
const postChanged = { 
...post,
owner: {
...post.owner,
name: 'Userka'
}
};

Все это приводит у сложнению мутаций в редьюсере, особенно если используется ngrx/entity. Проблемы связаны с тем, что нету предыдущего состояния. Его можно получить напрямую обратившись к словарю — state.entities[post.id], где может не быть сущности, что также приводит к дополнительным проверкам.

Основная цель — избегать изменения вложенных сущностей.

Не вкладывайте state в state.

Иногда разработчики делают сложные state, свойствами которого являются другие state:

interface CookiesState {
readonly loaded: boolean;
readonly cookies: SecurityTagEntity[] | null;
}

interface SignaturesState {
readonly loaded: boolean;
readonly signatures: SecurityTagEntity[] | null;
}

export interface SessionState {
readonly cookies: CookiesState;
readonly signatures: SignaturesState;
}

В данном случае еще просто управлять изменениями.

Однако если стейт расширяет EntityState, то все становится гораздо хуже.

interface PermissionsState extends EntityState<PermissionEntity> {
readonly loaded: boolean;
}

interface RolesState extends EntityState<RoleEntity> {
readonly loaded: boolean;
}
export interface State {
readonly permissions: PermissionsState;
readonly roles: RolesState;
}

Пример изменения state:

on(
attachPermissionSuccess,
(state, { roleId, permissionId }): State => ({
...state,
roles: rolesAdapter.updateOne(
{
id: roleId,
changes: {
permissions: {
...state.roles.entities[roleId]?.permissions,
[permissionId]: true,
},
},
},
{ ...state.roles, loaded: true },
),
}),
),

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

Не делайте редиректы в эффектах

В ngrx есть отдельное решение для навигации в приложении.

Если делать навигацию в эффектах с помощью router, то пропадает контроль над переходами в приложении.

sНапример, есть процесс оформления заказа из трех шагов:

  • /payment/start — подтверждение заказа;
  • /payment/process — оплата заказа;
  • /payment/finish — информации о заказе.

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

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

Используйте уникальный id в fetch в эффектах

В Nx есть оператор fetch, который позволяет обрабатывать http запросы используя различные стратегии. Однако в большинстве случаев он используется для ожидания ответа от сервера и генерации успешного или ошибочного экшена.

load$ = createEffect(() => {
return this.actions$.pipe(
ofType(PostActions.load),
fetch({
id: () => 'load',
run: () => this.postApiService.get().pipe(map((posts) => PostActions.loadSuccess({ posts }))),
onError: (action, error) => PostActions.loadFailure({ error }),
})
);
});

Свойство id позволяет отменять предыдущие api запросы.

Если например вызвать десять экшенов PostActions.load(), то девять будут отклонены и выполнится только последний. Если не использовать id в fetch, то будет запущено десять запросов параллельно.

Id получает экшен и другие данные из потока:

loadOne$ = createEffect(() => {
return this.actions$.pipe(
ofType(PostActions.load),
fetch({
id: ({ id }) => `load-${id}`,
run: ({ id }) => this.postApiService.getOne(id).pipe(
map((posts) => PostActions.loadSuccess({ posts }))
),
onError: (action, error) => PostActions.loadFailure({
error
}),
})
);
});

Избегайте использования интервалов в эффектах

Последней рекомендацией будет совет избегать использовать interval в эффектах.

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

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

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

Ссылки

Оглавление

Предыдущая статья — Концепты и понятия в Redux.

Следующая статья — Ngrx в действии: создание и тестирование.

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

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

Подписывайтесь на блог, чтобы не пропустить новые статьи про Angular, и веб-разработку. Medium | Telegram| VK |Tw| Ln

--

--

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

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