Entity state¶
Для эффективного управления массивами сущностей хранилища в NgRx имеется Entity State, который предоставляет собственное API для изменения этих сущностей и обеспечивает:
- меньшее по объему написание кода для создания редюсеров, которые обрабатывают массив сущностей;
- оптимизированное по производительности выполнение всех CRUD операций над массивом или его отдельным элементом.
NgRx Entity State устанавливается отдельно.
npm i @ngrx/entity --save
Entity State¶
Entity State представляет собой обычный интерфейс обобщенного типа, в котором обобщенным типом является модель (интерфейс) сущности, составляющей массив. Ниже приведен код интерфейса.
interface EntityState<V> {
ids: string[] | number[];
entities: { [id: string | id: number]: V };
}
Свойства интерфейса:
ids
- массив идентификаторов сущностей (по умолчаниюid
);entities
- объект со всеми сущностями, в котором ключ - идентификатор сущности, а значение - сама сущность.
Часть состояния, которая обрабатывает массивы сущностей, должна реализовывать интерфейс NgRx Entity State.
interface Article {
id: number;
user_id: number;
title: string;
}
export interface State extends EntityState<Article> {
currentArticle: number | null;
}
Entity Adapter¶
Доступ к API для работы с сущностями предоставляет экземпляр класса EntityAdapter
, который создается с явным указанием типа сущности.
export const adapter: EntityAdapter<Article> = createEntityAdapter<
Article
>();
Метод createEntityAdapter()
принимает необязательный параметр - объект с свойствами:
selectId
метод для выбора идентификатора сущности, обязателен, если у сущности отсутствует полеid
;sortComparer
- функция для сортировки сущностей в массива, но помните, что CRUD операции будут выполняться быстрее, если не придется сортировать массив.
export const adapter: EntityAdapter<Article> = createEntityAdapter<
Article
>({
selectId: (item) => item.id,
sortComparer: false, //явное указание, что сортировать массив не нужно
});
NgRx Entity Adapter имеет обширное API для работы с сущностями:
getInitialState()
- возвращает исходное состояние для массива сущностей заданного типа, параметром принимает объект со свойствами, которые также должны быть частью состояния;addOne()
- добавляет в массив новую сущность;addMany()
- добавляет несколько сущностей в массив;addAll()
- заменяет все текущие записи переданными;removeOne()
- удаляет одну указанную сущность;removeMany()
- удаляет несколько заданных сущностей;removeAll()
- удаляет все записи;updateOne()
- обновляет указанную сущность;updateMany()
- обновляет несколько заданных записей;upsertOne()
- обновляет переданную сущность, если она уже есть в массиве, в противном случае добавляет ее как новую;upsertMany()
- то же самое, что иupsertOne()
, только может принимать массив записей;map()
- применяет к каждой сущности массива переданную функцию, которая должна возвращать обновленную сущность.
Пример использования метода getInitialState()
.
export const initialState: State = adapter.getInitialState({
currentArticle: null,
});
Пример использования методов управления массивом.
article.actions.ts
export enum ArticleActionTypes {
LoadArticles = '[Articles Page] Load Articles',
AddArticle = '[Articles Page] Add Article',
UpsertArticle = '[Articles Page] Upsert Article',
AddArticles = '[Articles Page] Add Articles',
UpsertArticles = '[Articles Page] Upsert Articles',
UpdateArticle = '[Articles Page] Update Article',
UpdateArticles = '[Articles Page] Update Articles',
MapArticles = '[Articles Page] Map Articles',
DeleteArticle = '[Articles Page] Delete Article',
DeleteArticles = '[Articles Page] Delete Articles',
DeleteAllArticles = '[Articles Page] Delete All Articles',
}
export class LoadArticles implements Action {
readonly type = ArticleActionTypes.LoadArticles;
constructor(public payload: { articles: Article[] }) {}
}
export class AddArticle implements Action {
readonly type = ArticleActionTypes.AddArticle;
constructor(public payload: { article: Article }) {}
}
export class UpsertArticle implements Action {
readonly type = ArticleActionTypes.UpsertArticle;
constructor(public payload: { article: Article }) {}
}
export class AddArticles implements Action {
readonly type = ArticleActionTypes.AddArticles;
constructor(public payload: { articles: Article[] }) {}
}
export class UpsertArticles implements Action {
readonly type = ArticleActionTypes.UpsertArticles;
constructor(public payload: { articles: Article[] }) {}
}
export class UpdateArticle implements Action {
readonly type = ArticleActionTypes.UpdateArticle;
constructor(
public payload: { article: Update<Article> }
) {}
}
export class UpdateArticles implements Action {
readonly type = ArticleActionTypes.UpdateArticles;
constructor(
public payload: { articles: Update<Article>[] }
) {}
}
export class MapArticles implements Action {
readonly type = ArticleActionTypes.MapArticles;
constructor(
public payload: { entityMap: EntityMap<Article> }
) {}
}
export class DeleteArticle implements Action {
readonly type = ArticleActionTypes.DeleteArticle;
constructor(public payload: { id: string }) {}
}
export class DeleteArticles implements Action {
readonly type = ArticleActionTypes.DeleteArticles;
constructor(public payload: { ids: string[] }) {}
}
export class DeleteAllArticles implements Action {
readonly type = ArticleActionTypes.DeleteAllArticles;
}
export type ArticleActionsUnion =
| LoadArticles
| AddArticle
| UpsertArticle
| AddArticles
| UpsertArticles
| UpdateArticle
| UpdateArticles
| MapArticles
| DeleteArticle
| DeleteArticles
| DeleteAllArticles;
article.reducer.ts
interface Article {
id: number;
user_id: number;
title: string;
}
export interface State extends EntityState<Article> {
currentArticle: number | null;
}
export const adapter: EntityAdapter<Article> = createEntityAdapter<
Article
>();
export const initialState: State = adapter.getInitialState({
currentArticle: null,
});
export function articlesReducer(
state = initialState,
action: ArticleActionsUnion
): State {
switch (action.type) {
case ArticleActionTypes.LoadArticles: {
return adapter.addAll(action.payload.articles, state);
}
case ArticleActionTypes.AddArticle: {
return adapter.addOne(action.payload.article, state);
}
case ArticleActionTypes.UpsertArticle: {
return adapter.upsertOne(
action.payload.article,
state
);
}
case ArticleActionTypes.AddArticles: {
return adapter.addMany(
action.payload.articles,
state
);
}
case ArticleActionTypes.UpsertArticles: {
return adapter.upsertMany(
action.payload.articles,
state
);
}
case ArticleActionTypes.UpdateArticle: {
return adapter.updateOne(
action.payload.article,
state
);
}
case ArticleActionTypes.UpdateArticles: {
return adapter.updateMany(
action.payload.articles,
state
);
}
case ArticleActionTypes.MapArticles: {
return adapter.map(action.payload.entityMap, state);
}
case ArticleActionTypes.DeleteArticle: {
return adapter.removeOne(action.payload.id, state);
}
case ArticleActionTypes.DeleteArticles: {
return adapter.removeMany(action.payload.ids, state);
}
case ArticleActionTypes.DeleteAllArticles: {
return adapter.removeAll({
...state,
currentArticle: null,
});
}
default: {
return state;
}
}
}
Еще одним полезным методом NgRx Entity Adapter является getSelectors()
, который возвращает четыре селектора:
selectIds
- возвращает массив идентификаторов сущностей;selectEntities
- возвращает объект, в котором ключи это идентификаторы записей, а значения - сами записи;selectAll
- возвращает массив всех сущностей;selectTotal
- возвращает общее количество записей в массиве.
const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = adapter.getSelectors();
export const selectArticleIds = selectIds;
export const selectArticleEntities = selectEntities;
export const selectAllArticles = selectAll;
export const selectArticleTotal = selectTotal;