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

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
@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
@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$ = ...
}

Комментарии