Redux в Angular. Концепты и понятия в Redux.

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

В данной статье скажем пару слов о концептах и понятиях в Redux.

Если вы знакомы с redux, то можете смело переходить к следующим статьям.

Вся суть redux

В redux есть несколько понятий:

  • state — текущее состояние данных, которое обычно является обычным объектом;
  • store — сервис который хранит, изменяет и возвращает state.
  • action — объект или класс, который имеет свойство type и набор данных. Экшены нужны для того чтобы изменять state.
  • reducer — чистая функция, которая изменяет state в зависимости от полученного action’а.
  • mutation — синхронное изменение state. Обычно reducer как раз и содержит мутации для каждого из экшенов.
  • selector — обычно свойство связанное с конкретным свойством из state. При изменении свойства в state, selector тоже изменяется.

Принцип работы следующий:

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

Вводится понятие effect (side effects). В redux эффекты это обычно подписка на конкретный aсtion и после того как action был вызван, effect запускает какую-либо логику, например, делает dispatch нового action’а.

На схеме middleware представляет собой реализацию эффектов, где при вызове определенного экшена происходит вызов API запроса и после получения ответа от API вызывается новый action.

Реализация Redux

Сама реализация redux заключается в вызове экшенов, подписки на происходящие события и изменение состояния хранилища при конкретном типе экшена.

Обычно экшен это объект вида:

interface Action {
readonly type: string;
}

Обычно тип представляет собой название экшена и его назначение:

const action: Action = { type: '[Post] Load' };

Если action содержит данные, то интерфейс расширяется дополнительными свойствами:

interface CreateAction extends Action {
readonly data: Record<string, string>;
}

Тогда экшен будет содержать тип и передаваемое значение:

const action: CreateAction = { type: '[Post] Create', data: { id: 'uuid-1'} };

В первое время в реализациях redux использовались классы:

export class Load {
static readonly type = '[Post] Load';
}

export class LoadSuccess {
static readonly type = '[Post] Load Success';

constructor(public readonly posts: Post[]) {}
}

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

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

import { createAction, props } from '@ngrx/store';

import { Post, PostChange, PostCreate } from '@angular-samples/redux/posts/common';

export const load = createAction('[Post] Load');

export const loadSuccess = createAction('[Post] Load Success', props<{ posts: Post[] }>());
  • createAction — функция, которая создает экшен с требуемым типом.
  • props — утилита, которая позволяет сгенерировать корректный тип для экшена с передаваемыми свойствами. В данном случае loadSuccess будет экшен вида: { type: string, posts: Post[]}.

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

constructor(private readonly store: Store) {}

load(): void {
this.store.dispatch(load());
}

Изменения состояния происходят либо в reducer, либо в процессе обработки экшенов.

Классическая реализация подразумивает наличие редьюсера. Редьюсер представляет собой switch-case конструкцию, которая на конкретный тип экшена изменяет состояние стейта.

export function postReducer(state: PostState, action: Action): PostState {
let result: PostState;

switch (action.type) {
case '[Post] Load Success':
result = { ...state, loaded: true};
break;
default:
result = state ?? initialPostState;
}

return result;
}

В примере выше при событии [Post] Load Success происходит изменения флага loaded в стейте.

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

export interface PostState {
readonly loaded: boolean;
}

В современных реализациях чтобы не создавать множество case, используется функция создания редьюсера:

const reducer = createReducer(
initialPostState,
on(
loadSuccess,
(state, { posts }): PostState =>
(posts, {
...state,
loaded: true,
})
),
);

Полная реализация реализация с сохранением списка новостей:

const reducer = createReducer(
initialPostState,
on(
PostActions.loadSuccess,
(state, { posts }): PostState =>
postAdapter.setAll(posts, {
...state,
loaded: true,
})
),
on(PostActions.clearSuccess, (state): PostState => postAdapter.removeAll(state)),
on(PostActions.loadOneSuccess, PostActions.createSuccess, (state, { post }): PostState => postAdapter.upsertOne(post, state)),
on(PostActions.removeSuccess, (state, { uuid }): PostState => postAdapter.removeOne(uuid, state)),
on(
PostActions.changeSuccess,
(state, { post }): PostState =>
postAdapter.updateOne(
{
id: post.uuid,
changes: post,
},
state
)
)
);

Стоит отметить, что в примере используется postAdapter, который упрощает работу с изменением коллекций сущностей в стейте. Подробнее про adapter будет в статье про ngrx.

Для подписки на события обычно используется сервис Actions. В качестве actions выступает observable, который испускает все экшены, которые были задиспатчены.

constructor(private readonly actions: Actions) {}

loadSuccess$ = this.actions.pipe(
ofType(loadSuccess),
map(({ posts }) => posts)
);

Оператор ofType фильтрует экшены по типу и позволяет подписаться на конкретный action. В примере идет подписка на экшен loadSuccess.

В оператор можно передать и сам тип, а не action:

constructor(private readonly actions: Actions) {}

loadSuccess$ = this.actions.pipe(
ofType('[Post] Load Success'),
map(({ posts }: { posts: Post[] }) => posts)
);

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

Ссылки

Оглавление

Предыдущая статья — Создание UI компонентов для отображения новостей.

Следующая статья — Рекомендации по организации state.

Все исходники находятся на 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