Перейти к содержанию

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