【Angular】Subjectを使って離れているコンポーネント間で変更検知

# モチベーション

私、Angularを使ったWebフロント開発が2カ月目になります。ファイルの意味と関係を理解し、TypeScriptにも慣れてきて、自由自在に画面を作成できる！と意気込んだのですが、躓いたのがコンポーネント間でデータを受け渡すこと。親子のコンポーネントであれば、バインディングを利用して受け渡せることは分かりました。しかし、コンポーネント同士が並列関係である場合やコンポーネント同士が離れている場合は、バインディングが使えません。

自身の開発現場ではどうやって実現しているのだろうと調べたところ、面白い方法を使っていたので共有します。


# 必要な前提知識

Subjectを使った値の流し方
Observableを使った値の受け取り方
SubjectからObservableを生成する方法


# 実現したいこと

input-pageコンポーネントのボタンを押すことで、show-pageコンポーネントの数字を1つずつ増やします。要するにただのカウンターですが、設計の都合上input-pageとshow-pageは並列（共通の親コンポーネントを持つ）の関係になっているものとします。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539293/135a0907-e0ce-96c1-b0e2-bb15ef0f261c.png)

```html:app.component.html
<app-input-page></app-input-page>
<app-show-page></app-show-page>
```

共有したいデータモデルとして、ClickNumberを作成しています。これは、フィールドとしてクリックの合計回数を持つだけのシンプルなモデルです。実際には、様々なフィールドを持った情報群でも問題ありません。

```typescript:click-number.ts
export class ClickNumber {
    // 合計のクリック回数
    private _total: number;

    constructor() {
        this._total = 0;
    }

    get total(): number {
        return this._total;
    }

    set total(addedNumber: number) {
        this._total = addedNumber;
    }
}
```


# 実現方法

モデルの状態を管理するサービスを用意します。DetectChangeServiceは、ClickNumberの値が変更されたことをコンポーネントに伝達する役割を持ちます。フィールドとして、ClickNumberとSubjectを持ちます。Subjectは値を流す"川"のようなものですが、今回は「変更したことを伝えるお便りを流す」ために用意しました。実際に流しているのはvoidです。信じられないことに、voidでもちゃんとお便りの扱いになります。Subjectをインスタンス化するときの型次第で他の具体的なデータを流すこともできますよ。

```typescript:detect-change.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { ClickNumber } from '../model/click-number';
import { Observable, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DetectChangeService implements OnDestroy {

  private clickNumber = new ClickNumber();

  // "お便り"を流す川
  private changeDetector = new Subject<void>();

  constructor() { }

  // サービスが不要になったときに、
  // メモリを無駄に占領してしまうSubjectの処分
  ngOnDestroy(): void {
      this.changeDetector.unsubscribe();
  }

  // !!! 1
  // 加算と同時に、川に"お便り"を放流
  public addNumber(): void {
    this.clickNumber.total = this.clickNumber.total + 1;
    this.changeDetector.next();
  }

  public obtainNumber(): number {
    return this.clickNumber.total;
  }

  // !!! 2
  // "お便り"が流れる川の一部を提供
  // Subjectと同期している
  public obtainDetector(): Observable<void> {
    return this.changeDetector.asObservable();
  }
}
```

input-pageとshow-pageは次のようになっています。
input-pageには、ClickNumberの値を増やすクリックイベントだけが存在します。値の編集はDetectChangeServiceで管理されています。addNumber()メソッドは、加算と同時にnext()メソッドで「お便り」を"川"に流しています（// !!! 1）。

```typescript:input-page.component.ts
import { Component, OnInit } from '@angular/core';
import { DetectChangeService } from 'src/app/service/detect-change.service';

@Component({
  selector: 'app-input-page',
  templateUrl: './input-page.component.html',
  styleUrls: ['./input-page.component.css']
})
export class InputPageComponent {
  constructor(private readonly detectChangeService: DetectChangeService) {}

  onClickButton(): void {
    // !!! 1
    this.detectChangeService.addNumber();
  }
}
```

```html:input-page.component.html
<p>INPUT-PAGE</p>
<button (click)="onClickButton()">ADD</button>
```

```typescript:show-page.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { ClickNumber } from 'src/app/model/click-number';
import { DetectChangeService } from 'src/app/service/detect-change.service';

@Component({
  selector: 'app-show-page',
  templateUrl: './show-page.component.html',
  styleUrls: ['./show-page.component.css']
})
export class ShowPageComponent implements OnInit, OnDestroy {

  total!: number;

  subscription = new Subscription();

  constructor(private readonly detectChangeService: DetectChangeService) {}

  ngOnInit(): void {
    this.updateNumber(); // totalの更新

    this.subscription.add(
      // !!! 2
      // "お便り"が流れると、totalを更新
      this.detectChangeService.obtainDetector().subscribe(() => { 
        this.updateNumber();
      })
    );
  }

  // サービスが不要になったときに、
  // メモリを無駄に占領してしまうSubscriptionの処分
  ngOnDestroy(): void {
      this.subscription.unsubscribe();
  }

  // !!! 3
  // totalを更新
  private updateNumber(): void {
    this.total = this.detectChangeService.obtainNumber();
  }
}
```

```html:show-page.component.html
<p>SHOW-PAGE</p>
<p>Click：{{total}}</p>
```

show-pageは複雑ですが、ポイントは2つです。
1つ目は、"川"の観察者を設定することです。ngOnInit()メソッド（描画時に即実行される）の中で、DetectChangeServiceが提供するObservableをセットしています（// !!! 2）。
2つ目は、「お便り」が流れてきたときの挙動を予約することです。subscribe()メソッドの中では、観察者が「お便り」に気付いたときの対応がマニュアル化されています。今回は、updateNumber()メソッドを実行して値を書き換えます（// !!! 3）。


input-pageで、ADDボタンをクリックするとSubjectにvoidを流します。すると同じSubjectを見ているObservableがvoidを観測して、subscribe()内の処理を行います。

# まとめ

このようにコンポーネントの関係が親子でなくても、データを共有することができます。この方法のいいところは、以下の2点でしょうか。
1. データを受け取るコンポーネントがたくさんあっても、各々にObservableを設置すれば実現できる
2. データを受け取るコンポーネントが、データを流すコンポーネントから離れていても実現できる

余談ですが、一般に「変更したことを伝えるお便りを流す」ことを「通知する」と表現することがあります。