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

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