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

Effects

NgRx Effects реализуют побочные эффекты, работающие на основе библиотеки RxJS, применительно к хранилищу. Отслеживая поток действий, отправляемых в Store, они могут генерировать новые действия, например, на основе результатов выполнения HTTP-запросов или сообщений, полученных через Web Sockets.

Цели и функции NgRx Effects:

  • снять нагрузку с компонента по управлению состоянием и выполнению побочных эффектов и свести его работу к получению состояний и генерации действий;
  • отслеживание и фильтрация потока действий для выполнения побочного эффекта при возникновении определенного действия;
  • выполнение синхронных и асинхронных побочных эффектов.

NgRx Effects устанавливаются отдельно.

1
npm i @ngrx/effects

Чтобы полностью осознать все преимущества использования NgRx Effects, посмотрим на пример без них.

articles.service.ts

1
2
3
4
5
6
7
8
@Injectable({ providedIn: 'root' })
export class ArticlesService {
    constructor(private http: HttpClient) {}

    getArticles() {
        return this.http.get('/api/articles');
    }
}

articles.component.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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

1
2
3
4
@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}, действие, инициирующее выполнение эффекта, никогда не будет передано редюсеру.

1
@Effect({dispatch: false})

Жизненный цикл NgRx Effects

После регистрации всех эффектов в корневом модуле, происходит генерация действия ROOT_EFFECTS_INIT, для обработки которого может быть создан отдельный NgRx Effects.

1
2
3
4
5
@Effect()
initEffects$ = this.actions$.pipe(
    ofType(ROOT_EFFECTS_INIT),
    //...
);

NgRx предоставляет возможность управлять жизненным циклом эффекта с помощью реализации интерфейсов:

  • OnInitEffects — возвращает действие сразу после того, как эффект был зарегистрирован в приложении;
  • OnRunEffects — позволяет управлять началом и окончанием работы эффекта (по умолчанию начинается и заканчивается вместе с работой приложения);
  • OnIdentifyEffects — позволяет регистрировать NgRx Effects несколько раз (по умолчанию эффект регистрируется в Angular приложении один раз, независимо от того, сколько раз загружается сам класс эффекта).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@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, которое генерируется в момент регистрации эффекта и тем самым инициирует отслеживание потока действий.

В пределах одного класса может быть реализовано сразу несколько эффектов.

1
2
3
4
5
6
7
8
@Injectable()
export class ArticlesEffects{
    @Effect()
    loadArticles$ = ...

    @Effect()
    loadAuthors$ = ...
}

Комментарии