Effects¶
NgRx Effects реализуют побочные эффекты, работающие на основе библиотеки RxJS, применительно к хранилищу. Отслеживая поток действий, отправляемых в Store
, они могут генерировать новые действия, например, на основе результатов выполнения HTTP-запросов или сообщений, полученных через Web Sockets.
Цели и функции NgRx Effects:
- снять нагрузку с компонента по управлению состоянием и выполнению побочных эффектов и свести его работу к получению состояний и генерации действий;
- отслеживание и фильтрация потока действий для выполнения побочного эффекта при возникновении определенного действия;
- выполнение синхронных и асинхронных побочных эффектов.
NgRx Effects устанавливаются отдельно.
npm i @ngrx/effects
Чтобы полностью осознать все преимущества использования NgRx Effects, посмотрим на пример без них.
articles.service.ts
@Injectable({ providedIn: 'root' })
export class ArticlesService {
constructor(private http: HttpClient) {}
getArticles() {
return this.http.get('/api/articles');
}
}
articles.component.ts
@Component({
selector: 'app-articles',
template: `
<ul>
<li
*ngFor="let item of articles"
[textContent]="item.title"
></li>
</ul>
`,
})
export class ArticlesComponent {
articles: Article[] = [];
constructor(private articlesService: ArticlesService) {
this.getArticles();
}
getArticles() {
this.articles = [];
this.articlesService.getArticles().subscribe(
(items) => (this.articles = items),
(err) => console.log(err)
);
}
}
А теперь изменим пример внедрением NgRx Effects (ArtilcesService
остается неизменным).
articles.component.ts
@Component({
selector: 'app-articles',
template: `
<ul>
<li
*ngFor="let item of articles"
[textContent]="item.title"
></li>
</ul>
`,
})
export class ArticlesComponent {
articles$: Observable = this.store.pipe(
select(selectArticlesList)
);
constructor(private store: Store) {
this.store.dispatch(new LoadArticles());
}
}
articles.actions.ts
export enum ArticlesActions {
LoadArticles = '[Articles Page] Load Articles',
ArticlesLoadedSuccess = '[Articles Page] Articles Loaded Success',
ArticlesLoadedError = '[Articles Page] Articles Loaded Error',
}
export interface Article {
id: number;
author: string;
title: string;
}
export class LoadArticles implements Action {
readonly type = ArticlesActions.LoadArticles;
}
export class ArticlesLoadedSuccess implements Action {
readonly type = ArticlesActions.ArticlesLoadedSuccess;
constructor(public payload: { articles: Article[] }) {}
}
export class ArticlesLoadedError implements Action {
readonly type = ArticlesActions.ArticlesLoadedError;
}
export type ArticlesUnion =
| LoadArticles
| ArticlesLoadedSuccess
| ArticlesLoadedError;
articles.reducer.ts
export interface ArticlesState {
list: Article[];
}
const initialState: ArticlesState = {
list: [],
};
export function articlesReducer(
state: State = initialState,
action: ArticlesUnion
) {
switch (action.type) {
case ArticlesActions.ArticlesLoadedSuccess:
return {
...state,
list: action.payload.articles,
};
case ArticlesActions.ArticlesLoadedError:
return {
...state,
list: [],
};
default:
return state;
}
}
const selectArticles = (state: State) => state.articles;
export const selectArticlesList = createSelector(
selectArticles,
(state: ArticlesState) => state.list
);
articles.effects.ts
@Injectable()
export class ArticlesEffects {
@Effect()
loadArticles$ = this.actions$.pipe(
ofType(ArticlesActions.LoadArticles),
mergeMap(() =>
this.articlesService.getArticles().pipe(
map(
(articles) =>
new ArticlesLoadedSuccess({
articles: articles,
})
),
catchError(() => of(new ArticlesLoadedError()))
)
)
);
constructor(
private actions$: Actions,
private articlesService: ArticlesService
) {}
}
app.module.ts
@NgModule({
imports: [EffectsModule.forRoot([ArtilcesEffects])],
})
export class AppModule {}
Создание NgRx Effect начинается c отслеживания потока событий, который представлен сервисом Actions
и предваряется декоратором @Effect()
. Далее с помощью оператор ofType()
задается тип действия, при возникновении которого будет выполнен побочный эффект, который в свою очередь должен возвращать новое действие, передаваемое далее в хранилище. Также не забывайте обрабатывать ошибки.
Сначала действие обрабатывается редюсером, а только потом попадает в поток сервиса Actions
.
Сравнив два примера, сразу станет очевидно, что применение NgRx Effects избавило компонент от необходимости самостоятельно обращаться к сервису и контролировать результат его работы.
Все NgRx Effects должны регистрироваться в приложении с помощью модуля EffectsModule
. Если вы определяете эффекты на уровне корневого модуля, то необходимо использовать метод forRoot()
, если на уровне второстепенного - forFeature()
. Оба метода принимают массив эффектов к качестве параметра.
Если в приложении все NgRx Effects определены для второстепенных модулей, то в корневом модуле обязательно должен быть импортирован без аргументов метод EffectsModule.forRoot()
.
Эффекты начинают следить за потоком действий сразу после загрузки модуля, к которому они относятся.
Если вам необходимо реализовать побочный эффект, но новое действие генерировать не нужно, передайте декоратору @Effect()
объект с указанием значения false
для свойства dispatch
.
В случае задания {dispatch: false}
, действие, инициирующее выполнение эффекта, никогда не будет передано редюсеру.
@Effect({dispatch: false})
Жизненный цикл NgRx Effects¶
После регистрации всех эффектов в корневом модуле, происходит генерация действия ROOT_EFFECTS_INIT
, для обработки которого может быть создан отдельный NgRx Effects.
@Effect()
initEffects$ = this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
//...
);
NgRx предоставляет возможность управлять жизненным циклом эффекта с помощью реализации интерфейсов:
OnInitEffects
- возвращает действие сразу после того, как эффект был зарегистрирован в приложении;OnRunEffects
- позволяет управлять началом и окончанием работы эффекта (по умолчанию начинается и заканчивается вместе с работой приложения);OnIdentifyEffects
- позволяет регистрировать NgRx Effects несколько раз (по умолчанию эффект регистрируется в Angular приложении один раз, независимо от того, сколько раз загружается сам класс эффекта).
@Injectable()
export class ArticlesEffects
implements OnInitEffects, OnRunEffects {
@Effect()
loadArticles$ = this.actions$.pipe(
ofType(ArticlesActions.LoadArticles),
startWith(new LoadArticles()),
mergeMap(() =>
this.articlesService.getArticles().pipe(
map(
(articles) =>
new ArticlesLoadedSuccess({
articles: articles,
})
),
catchError(() => of(new ArticlesLoadedError()))
)
)
);
constructor(
private actions$: Actions,
private articlesService: ArticlesService
) {}
ngrxOnInitEffects(): Action {
return new ArticlesEffectsInit();
}
ngrxOnRunEffects(
resolvedEffects$: Observable<EffectNotification>
) {
return this.actions$.pipe(
ofType(ArticlesActions.ArticlesEffectsInit),
exhaustMap(() =>
resolvedEffects$.pipe(
takeUntil(
this.actions$.pipe(
ofType(ArticlesActions.ArticlesLoadedSuccess)
)
)
)
)
);
}
}
Здесь для демонстрации работы OnInitEffects
и OnRunEffects
было введено дополнительное действие ArticlesEffectsInit
, которое генерируется в момент регистрации эффекта и тем самым инициирует отслеживание потока действий.
В пределах одного класса может быть реализовано сразу несколько эффектов.
@Injectable()
export class ArticlesEffects{
@Effect()
loadArticles$ = ...
@Effect()
loadAuthors$ = ...
}