Тестовое задание на Angular. Использование Ngrx State.

Aleksandr Serenko
F.A.F.N.U.R
Published in
4 min readJul 5, 2021

В данной статье рассмотрим использование Ngrx State. Создадим feature state для базовых сущностей (room, building, person).

После того, как было создано хранилище, можно создать state.

Смысл работы room state в следующем:

  • Необходимо прочитать данные из localStorage и сохранить все данные в RoomState
  • Предоставить доступ к данным state, в частности к комнатам
  • При получении ошибки во время загрузки, как-то реагировать на это.

Создадим новый модуль room-state, который будет располагаться в rooms/state:

ng g m rooms/state

Переименуем файл, а также создадим для него файл с тестами.

Далее, добавим Ngrx фичу, которую назовем room:

ng generate @ngrx/schematics:feature room --module=rooms/state/room--state.module.ts

Для меня было большим удивлением, что в стандартных схематиках Ngrx, есть тесты на action’ы. Зачем они там нужны, не понятно.

Базовая имплементация

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

RoomActions будут выглядеть так:

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

Сама цепочка событий будет следующей:

  • Запустить загрузку номеров, испустив событие loadRooms
  • Если номера загружены, то испустить событие loadRoomsSuccess
  • Если при загрузке номеров произошла ошибка, испустить событие loadRoomsFailure

Отметим, что в качестве payload используется утилита payload вместо стандартной — props. Это необходимо затем, чтобы разделить сам action от данных, передаваемых в него. То есть:

export const loadPersons = createAction('[Person] Load Persons');

формально представляет собой:

{ type: '[Person] Load Persons' }

Если использовать props’ы:

export const loadPerson = createAction('[Person] Load Persons', props<{ id: number }>());

то action будет:

{ type: '[Person] Load Persons', id: number }

Утилита payload позволяет сделать следующее:

export const loadPerson = createAction('[Person] Load Persons', payload<{ id: number }>());

привести action к виду:

{ type: '[Person] Load Persons', payload: { id: number } }

Замечу, что это заморочки автора, и вы вполне можете использовать props’ы.

Экшены созданы, теперь опишем структуру reducer’а, приведя room.reducer.ts к виду:

В данном случае, был создан RoomState:

export interface RoomState {  
rooms: Room[] | null;
roomsLoadError: Record<string, any> | null;
}

Для изменений значений в state используется функция редьюсер, которая получая определенный action, изменяет state требуемым способом.

export const reducer = createReducer(roomInitialState,  
on(RoomActions.loadRooms, (state) => ({
...state,
roomsLoadError: null,
})),
on(RoomActions.loadRoomsSuccess, (state, { payload }) => ({
...state,
rooms: payload,
})),
on(RoomActions.loadRoomsFailure, (state, { payload }) => ({
...state,
roomsLoadError: payload,
})
));

При запуске события loadRoomSuccess в state будут записаны номера. А если произошла ошибка, то соответственно в state запишется она, чтобы потом ее можно было показать пользователю.

Добавим select’оры, которые позволят обращаться к данным из state’а:

И в конце, создадим эффект, который будет брать данные из localStorage и сохранять их в state:

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

В данном примере, эффект слушает вызов события loadRooms. Как только происходит вызов данного экшена, эффект загружает номера, оборащаясь к roomStorage.get(), который возвращает Observable<Room[]>. После того, как данные будут получены, эффект испустит новое событие loadRoomSuccess, куда передаст загруженные номера.

И для того, чтобы напрямую не трогать state, создадим Facade, в котором предоставим доступ к dispatch и select’орам:

Формально фасад содержит методы для диспача событий, а также предоставляет доступ к данным state.

В итоге получаемRoomStateModule, в котором подключены эффекты и фасад.

Полная реализация

Базовом примере приведен основной концепт как создавать action’ы, добавлять данные в state и вызывать и обрабатывать асинхронные события на примере загрузки сущностей из локального хранилища.

Теперь реализуем полный список action’ов, который потребуется в приложении, в данном случае некий CRUD:

  • загрузка сущностей
  • удаление всех сущностей
  • изменение сущности
  • удаление сущности
  • добавление сущности

Тогда RoomActions:

RoomReducer:

Внимательный читатель заметит, что в reducer’е мы стали использовать @ngrx/entity. Это библиотека, которая упрощает работу над коллекциями сущностей. В данном случае нам нужно хранить в state’е несколько сущностей номеров (RoomEntity). И например, при изменении одной сущности, достаточно указать id сущности и поля, которые изменились, для того чтобы её изменить. Иначе, пришлось бы сначала найти сущность в коллекции, потом изменить ее, и перезаписать коллекцию целиком. Ngrx/entity упрощает это.

RoomSelectors:

Как видно из примера, Ngrx/entity добавляет несколько методов для работы с коллекциями и в селекторы.

RoomEffect’ы:

И RoomFacade:

Аналогично создадим Building State:

и Person State:

Реализации BuildingState и PersonState очень похожи на RoomState, за исключением ряда action’ов, которые связаны с выбором конкретных сущностей, например выбрать все отели по собственнику и т.д.

Ссылки

Вернуться к оглавлению — Введение.

Следующая статья — Работа с данными из State.

Предыдущая статья — Использование localStorage.

Все исходники на github/fafnur/barinb.

Группа в Medium: https://medium.com/fafnur
Группа в Vkontakte: https://vk.com/fafnur
Группа в Facebook: https://www.facebook.com/groups/fafnur/
Telegram канал: https://t.me/f_a_f_n_u_r
Twitter: https://twitter.com/Fafnur1
LinkedIn: https://www.linkedin.com/in/fafnur

--

--

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

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