Что такое ngrx/createFeature и как это может облегчить жизнь frontend-разработчику
Хабр, привет!
В этой статье будет описана одна очень полезная вещь, способная упростить жизнь разработчику, которому приходилось (или приходится) работать с библиотекой NgRx. Сейчас это достаточно популярно и востребованно. В частности, я покажу, как избавится от привычного описания selectors, сокращения кода по части reducers и, уверен, результат удивит.
Что такое NgRx feature?
Начнем с общих понятий и поговорим о том, как всё устроено.
Существует три основных строительных блока глобального управления состоянием в вашем проекте при использовании @ngrx/store:
Действия – actions;
Редюсеры – reducers;
Селекторы – selectors.
Для вынесения определенной логики из компонента, обработки побочных эффектов и т.д. существуют еще и effects. Но о них сейчас речи идти не будет.
Для определенного состояния объекта мы создаем reducer. Он обрабатывает переходы состояния на основе отправленных actions. При помощи selectors у нас есть возможность отслеживать изменения фрагментов этого определенного состояния, а также их получать.
Более подробно углубляться в эту тему не буду, перейдем к описанию более сложных вещей, понимание которых как раз-таки строится на полном освоении базовых понятий.
Помимо всех вышеописанных действий, нам нужно определить имя фичи, необходимое для регистрации reducers в хранилище.
Мы можем рассматривать NgRx Feature как группировку её имени, reducer и selector для конкретного состояния. То есть это полностью укомплектованная всем функционалом часть хранилища, обособленная от таких же частей, которая отвечает за хранение необходимых переменных, их обработку, а также всю сопутствующую при этом логику.
В проектах любой сложности возникает такая ситуация, когда определенный фрагмент состояния содержит какое-то количество полей, не требующих вычислений и не участвующих в них. То есть они добавляются в начальное состояние, при помощи actions изменяются и используются базовыми selectors в компонентах или сервисах. Selectors этих полей просто берут их из фрагмента store, за который отвечает их featureSelector и возвращают нам.
Все эти селекторы необходимо вручную прописывать, делают они одно и тоже, различие лишь в том, для какого поля они описаны.
Создание функциональных элементов по старинке
Создание файла редюсеров
Итак, создадим таким образом редюсер для фрагмента стора, отвечающего за создание заявок.
Воспользуемся createReducer функцией из пакета @ngrx/store.
На самом деле для кого-то может и использование этой функции уже показаться довольно прогрессивным, но мы пойдем дальше.
import { createReducer, on } from '@ngrx/store';
import { ApplicationToSendActions } from "./applicationToSend.actions";
import { Applications } from "../../../../models/applications.model";
export namespace applicationToSendReducer {
export const applicationToSendFeatureKey = 'applicationToSend';
export interface ApplicationToSendState {
applicationView: Applications;
applicationFirstPart: any;
applicationSecondPart: any;
applicationThirdPart: any;
}
export const initialState: ApplicationToSendState = {
applicationView: null,
applicationFirstPart: null,
applicationSecondPart: null,
applicationThirdPart: null
}
export const reducer = createReducer(
initialState,
on( ApplicationToSendActions.addFirstPartToApplication,( state, { applicationFirstPart } ) => ( { ...state, applicationFirstPart } ) ),
on( ApplicationToSendActions.addSecondPartToApplication, ( state, { applicationSecondPart } ) => ( { ...state, applicationSecondPart } ) ),
on( ApplicationToSendActions.addThirdPartToApplication, ( state, { applicationThirdPart } ) => ( { ...state, applicationThirdPart } ) ),
on( ApplicationToSendActions.addApplicationViewObject, ( state, { applicationView } ) => ( { ...state, applicationView } ) )
)}
Чтобы зарегистрировать этот редуктор в хранилище NgRx, мы используем StoreModule.forFeature метод.
store.module.ts
StoreModule.forFeature( applicationToSendReducer.applicationToSendFeatureKey, applicationToSendReducer.reducer )
Куда передаем название фичи, а также сам редюсер.
Создание файла селекторов
Чтобы выбрать поля состояния создадим селектор объектов, дочерние селекторы, а также селектор модели представления:
selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { applicationToSendReducer } from "./applicationToSend.reducer";
export namespace applicationToSendSelectors {
export const applicationToSendState =
createFeatureSelector<applicationToSendReducer.ApplicationToSendState>
( applicationToSendReducer.applicationToSendFeatureKey );
export const applicationFirstPartSelector = createSelector( applicationToSendState, ( { applicationFirstPart } ) => applicationFirstPart )
export const applicationSecondPartSelector = createSelector( applicationToSendState, ( { applicationSecondPart } ) => applicationSecondPart )
export const applicationThirdPartSelector = createSelector( applicationToSendState, ( { applicationThirdPart } ) => applicationThirdPart )
export const applicationViewSelector = createSelector( applicationToSendState, ( { applicationView } ) => applicationView )
}
Как видно несколько базовых селекторов по типу взял из хранилища одно поле, и отдал в компонент или сервис. Выглядит не громоздко, однако их всего 4 штуки, а как бы это выглядело с большим количеством селекторов. Огромная куча одинакового повторяющегося кода, отличающегося только именами полей, которые там пробрасываются. К тому же каждый разработчик в своем проекте придерживается своей политики наименований, и не всегда по названию селектора можно определить за что он отвечает, и вообще селектор ли это.
Использование Feature Creator
Давайте теперь посмотрим, как добиться того же результата, но уже без вышеописанных проблем.
Для начала проведем небольшой рефаткоринг редюсера.
Изменения в файле редюсеров
reducer.ts
import { createFeature, createReducer, on } from '@ngrx/store';
import { ApplicationToSendActions } from "./applicationToSend.actions";
import { Applications } from "../../../../models/applications.model";
export interface ApplicationToSendState {
applicationView: Applications;
applicationFirstPart: any;
applicationSecondPart: any;
applicationThirdPart: any;
}
export const initialState: ApplicationToSendState = {
applicationView: null,
applicationFirstPart: null,
applicationSecondPart: null,
applicationThirdPart: null
}
export const applicationToSendFeature = createFeature({
name: "applicationToSend",
reducer: createReducer(
initialState,
on( ApplicationToSendActions.addFirstPartToApplication, ( state, { applicationFirstPart } ) => ( { ...state, applicationFirstPart } ) ),
on( ApplicationToSendActions.addSecondPartToApplication, ( state, { applicationSecondPart } ) => ( { ...state, applicationSecondPart } ) ),
on( ApplicationToSendActions.addThirdPartToApplication, ( state, { applicationThirdPart } ) => ( { ...state, applicationThirdPart } ) ),
on( ApplicationToSendActions.addApplicationViewObject, ( state, { applicationView } ) => ( { ...state, applicationView } ) )
)
})
Уберем лишнюю обертку для редюсера, а также поле applicationToSendFeatureKey.
Вместо этого используем функцию createFeature, принимающую в себя обьект с полями name – имя фичи, фрагмента хранилища, за который отвечает редюсер, а так же поле reducer, где описаны редюсеры для определенных actions.
Уже сейчас видно, что код стал более лаконичным, все основные сущности помещены в одно место и кроме интерфейса фрагмента хранилища, его начального состояния, и самого обьекта редюсера в файле ничего нет.
Регистрация фичи в модуле
Теперь для регистрации фичи достаточно в модуле передать один обьект, который мы создали.
store.module.ts
StoreModule.forFeature( applicationToSendFeature )
Изменения в файле селекторов
А теперь самое главное: простота и удобство, которое вносит createFeature больше всего заметно в файле селекторов.
Вспомним, что до использования этой функции селекторы представляли собой нагромождение повторяющегося одинакового кода.
А теперь файл селекторов выглядит так:
selectors.ts
import {applicationToSendFeature} from "./applicationToSend.reducer";
const {
selectApplicationToSendState,
selectApplicationThirdPart,
selectApplicationSecondPart,
selectApplicationFirstPart,
selectApplicationView
} = applicationToSendFeature
export const applicationToSendSelectors = {
selectApplicationThirdPart,
selectApplicationSecondPart,
selectApplicationFirstPart,
selectApplicationView,
selectApplicationToSendState
}
Помимо сокращения импортов, существенно преобразился код.
Итак, по порядку:
Функция createFeature убирает необходимость в такой сущности как featureSelector.
Теперь не нужно прописывать вручную селектор для каждого поля хранилища, ведь они создаются автоматически.
К именам полей хранилища добавляются приставки select, а к селектору фичи помимо приставки добавляется и суффикс State.
Так, имя селектора фичи у нас это selectApplicationToSendState, а имена дочерних селекторов: selectApplicationThirdPart,selectApplicationSecondPart, selectApplicationFirstPart.
Ничего писать больше не нужно, достаточно обьявить все селекторы в файле, и затем экспортировать необходимые для дальнейшего использования.
Более того, из этих селекторов можно создавать свои, более сложные селекторы.
Теперь достаточно заменить имена селекторов в компонентах, где они использовались на новые. Они доступны точно таким же образом как и в прошлом варианте их использования.
Заключение
Создатели функций сокращают количество повторяющегося кода в файлах селекторов, используя возможности шаблонных литеральных типов, представленных в TypeScript версии 4.1.1. Это может значительно улучшить ваш код, особенно при больших состояниях функций.