AngularのサービスクラスでEventEmitterを使ってはいけない


TL;DR

テンプレート以外からイベントの購読処理を行うケースでは,rxjsを使いましょう.
EventEmitterを使うのは危険です

EventEmitterについて

EventEmitterは,コンポーネントからその上位コンポーネントへの,任意の値の通知を提供します.
つまり,

child.component.ts
   @Output()
   someAction = new EventEmitter<SomeObject>();

   func() {
     this.someAction.emit({ foo: 'bar' })
   } 

のようにしておけば,その上位コンポーネントで

higher.component.html
<app-child (someAction)="onSomeAction($event)"></app-child>

のように値を受け取れる,というわけです.

サービスクラスでの通知処理

では,コンポーネントではなく,サービスクラス等から,何らかの値を通知したい場合を考えます.
先程との違いは,実際の購読処理を行う責務が誰にあるかです.
先程での例では,開発者はテンプレート内で通知の処理方法を記述し,実際の購読処理を行う責務はAngularにありました.
今回の場合,実際の購読処理を行う責務は開発者にあります.

これを踏まえて,先程のEventEmitterを使うと,以下のように書けそうです.

以下は間違った例です

test.service.ts
class TestService {
  someChange = new EventEmitter<SomeObject>();
}
testService.someChange.subscribe(obj => ...)

確かにEventEmitterSubject<T>を継承していますが,このように書くのは間違いです.
EventEmitterの購読方法はあくまで内部的なもので,これからも同じ方法で購読できる保証はありません.
https://stackoverflow.com/a/36076701/3094842

つまり,EventEmitterの購読処理を行う責務を,開発者が負うことはできない,ということです.
これについて,このようなケースでは,単にrxjsを使うと良いようです.

先程のコードをrxjsを使用して書き直すと,以下のようになります.

class TestService {
  private internalSomeObject: SomeObject;
  someChange$ = new BehaviorSubject<SomeObject>(undefined);

  get someObject() { return this.internalSomeObject; }
  set someObject(someObject: SomeObject) {
    this.someChange$.next(someObject);
    this.internalSomeObject = someObject;
  }
}

これで,コンポーネントから認証情報の変更を検知できるようになります.

testService.someChange$.subscribe((someObject: SomeObject) => {
  // 変更を検知したときの処理
});