Тестовое задание на Angular. Использование Ngrx State.
В данной статье рассмотрим использование 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