Redux в Angular. Концепты и понятия в 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