Blog

ブログ

Angular講座:コンポーネント間の値受け渡し④

2022.07.28 公開
Momoka_Ide
Momoka_Ide プログラマ

おさらい

コンポーネント間の値受け渡しをテーマに、初心者向けのサンプルコードを添えて色々な渡し方を紹介しています。
過去の記事一覧はこちら

今回はRxJSを用いたコンポーネント間の値受け渡しをしてみようと思います。

RxJSについて

ObservableとSubjectの違い

RxJSについて調べると出てくる単語、ObservableやSubjectなど…
それぞれについて調べてみて下さい。専門用語が多く、なかなか説明が難しい。。。

専門用語を使わず、なんとなくそういうものなんだな、と理解していただく例えをすると
Observable は 新聞屋、Subject は 配達業者
…って感じのイメージです。

Observableは最初に定義されたデータを流しますが、Subjectは任意のタイミングで値を流し、受け取ることができます。新聞屋は新聞を配達をするだけ、配達業者は荷物を受け取り、配達をしますね。

今回はコンポーネント間の値受け渡しというテーマですので、任意のタイミングに値を送信し、受け取るという機能を実装します。なので、上記の例えで言うとやりたいことは配達ですね。Subjectを利用してサンプルを作成していきます。

Subjectの種類

Subjectにも種類があり、Subject、BehaviorSubject、ReplaySubject、AcyncSubjectなどがあります。
今回はSubject,BehaviorSubjectについて、掘り下げます

SubjectとBehaviorSubject

使い分けのポイント
・初期値の有無
・最後に流された値の再取得が必要かどうか

private _user$ = new Subject<User>();
private _user$ = new BehaviorSubject<User | null>(null);

Subjectは初期値を必要としませんが、BehaviorSubjectは<>で指定した型に一致する初期値を引数に求められます。
subscribeしたとき、Subjectはnext()で値が流されるまで何も流れてきませんが、BehaviorSubjectは、subscribeするとその時点での最も新しい値が流れてきます(一度もnext()を実行してない場合、初期値。)

subscribeとは……?という人は、配達業者に、ここに配達してくださいと頼んだ状態とでもイメージしてください。next()は配達の依頼のようなものです。

また、Subjectにはなく、BehaviorSubjectにだけある関数に、getValue()があります。
Subjectはnext()で値を流したとき、何を流したのか値を保持しませんが、BehaviorSubjectは値を保持し、getValue()を使うことでその時点で最新の値を取得することができます。

・subscribeするとき、next()が動くまでの間に初期値が欲しい → BehaviorSubject
・値を保持しておき、好きな時に値を取得したい → BehaviorSubject
・値の保持は必要なく、値を流すだけでいい → Subject

Subscription

値を取得する

Subjectで流れてきた値を受け取る方法をhtml,ts両方で確認していきます。

export class UserSubjectService {
private _user$ = new Subject<User>();  
get user$() {
    return this._user$.asObservable();
  }
  // 値を流す
  next(user: User) {
    return this._user$.next(user);
  }
}

export interface User {
  userId: string;
  name: string;
  email: string;
}

値を受け取るためのObservableクラスは、asObservable()関数を用いることで取得できます。

tsファイル側で取得する

export class SampleComponent implements OnInit, OnDestroy {
  
  subsc: Subscription = new Subscription();
  userObs$: Observable<User>;

  constructor(
    private userObserver: UserObservableService
  ) {
    this.userObs$ = this.userObserver.user$;
  }

  async ngOnInit() {
   this.subsc.add(
      this.userObs$.subscribe((user) => {
        console.log(user); // next()で値が流されてきたらコンソール出力する
      })
    );
}
  ngOnDestroy(): void {
    // 購読停止
    this.subsc.unsubscribe();
  }

}

htmlファイル側で取得する

// sample.component.html  
<ng-container *ngIf="userObs$ | async as user">
    <div>
      <span>{{ user.userId }}</span>
      <span>{{ user.name }}</span>
      <span>{{ user.email }}</span>
    </div>
  </ng-container>

asyncパイプを使うことで、userObs$に値が流れてき次第、表示を行うことができます。

購読を停止する

上記コードのこの部分に注目してください。

  ngOnDestroy(): void {
    // 購読停止
    this.subsc.unsubscribe();
  }

unsubscribe()を実行することで、値の取得が停止されます。

また、上記のコードではadd()関数の中でsubscribeをしています。
同じことをしている他の書き方として、下記のような方法もあります。Subscription型の変数に、subscribeの処理を挿入しています。

  subsc:Subscription;
  constructor(
    private userObserver: UserObservableService
  ) {
    this.subsc = this.userObserver.user$.subscribe(user=>{
      console.log(user);
    })
  }
ngOnDestroy(): void {
    // 購読停止
  if(this.subsc){
     this.subsc.unsubscribe();
  }
}

この二つの何が違うかというと、もしuser$以外のObservableからも値をsubscribeしたいときに、後者の書き方だとObservableの数だけSubscription型の変数を用意してあげないといけません。そしてngOnDestroyで、Observableの数だけ用意された全てのsbsc変数に対して、unsubscribe()の実行が必要です。

  subscA:Subscription;
  subscB:Subscription;
  constructor(
    private userObserver: UserObservableService
  ) {
    this.subscA = this.userObserver.A$.subscribe(valueA=>{
      console.log(valueA);
    });
    this.subscB = this.userObserver.B$.subscribe(valueB=>{
      console.log(valueB);
    });
  }
ngOnDestroy(): void {
  if(this.subscA){
     this.subscA.unsubscribe();
  }
  if(this.subscB){
     this.subscB.unsubscribe();
  }
}

必要な数だけ、destroyされるときの手間と変数が増える……
一方で、addを使った書き方をしておくと、Subscriptionの変数は1つで済み、destroy時も1回呼べばOKです。
addを使ってsubscribeをする癖をつけておくと、unsubscribeのし忘れがなくなりそうです。

  subsc = new Subscription();
  constructor(
    private userObserver: UserObservableService
  ) {
    this.subsc.add(this.userObserver.A$.subscribe(valueA=>{
      console.log(valueA);
    }));
    this.subsc.add(this.userObserver.B$.subscribe(valueB=>{
      console.log(valueB);
    }));
  }
ngOnDestroy(): void {
   this.subsc.unsubscribe(); // AとB両方がunsubscribeされる
}

subscribeの数が複数あるときは積極的にaddを使った書き方をしていきましょう。

unsubscribe()しないとどうなるのか

subscribeを実装しているページにアクセスするたびに、subscribeされる数が増えていき、メモリリークを引き起こします。

 this.subscA = this.userObserver.A$.subscribe(valueA=>{
      console.log('valueA');
});

例えばこのコードがあるページで、unsubscribeを行なっていなかったとしましょう。
初回アクセス時のコンソール出力は、

valueA

別のページに遷移した後、2回目のアクセス時のコンソール出力は、

valueA
valueA

アクセスした回数分、subscribeに書いた処理が実行されます。

サンプルコード

css省略しています
child1コンポーネントのフォームに入力し、ボタンを押したらchild2コンポーネントに送信されるサンプルです。

$ ng g c child1
$ ng g c child2
$ ng g s service/user-subject
// app.component.html
<app-child1></app-child1>
<app-child2></app-child2>
//child1.component.html
<div class="wrapper">
  <div class="column">
    <div class="items">
      <label for="userId">ユーザID</label>
      <input id="userId" type="text" [(ngModel)]="userId" />
    </div>
    <div class="items">
      <label for="name">名前</label>
      <input id="name" type="text" [(ngModel)]="name" />
    </div>
    <div class="items m-none">
      <label for="email">メールアドレス</label>
      <input id="email" type="text" [(ngModel)]="email" />
    </div>
  </div>
  <div class="btn-box">
    <button type="button" (click)="save()">保存</button>
  </div>
</div>
//child1.component.ts
export class Child1Component implements OnInit {
  userId = '';
  name = '';
  email = '';

  constructor(
    private userSubject: UserSubjectService
  ) {}

  ngOnInit(): void {}

  /** rxjsに値を流す */
  save() {
    const user: User = {
      userId: this.userId,
      name: this.name,
      email: this.email,
    };
    this.userSubject.next(user);
    this.clear();
  }

  /** フォームをリセット */
  clear() {
    this.userId = '';
    this.name = '';
    this.email = '';
  }
}
// child2.component.html
<div class="wrapper">
  <div class="title">Subject</div>
  <ng-container *ngIf="user$ | async as user">
    <div class="column">
      <span class="items">{{ user.userId }}</span>
      <span class="items">{{ user.name }}</span>
      <span class="items m-none">{{ user.email }}</span>
    </div>
    <!-- <div class="btn-box">
        <button type="button" (click)="putOutUser()">再取得</button>
      </div> -->
  </ng-container>
</div>
// child2.component.ts
export class Child2Component implements OnInit,OnDestroy {
  user$: Observable<User | null>;
  subsc: Subscription = new Subscription();

  constructor(
    private userSubject: UserSubjectService,
  ) {
    this.user$ = this.userSubject.user$;
  }

  ngOnInit(): void {
    this.subsc.add(this.user$.subscribe(s=>{
      console.log(s);
    }))
  }

  ngOnDestroy(): void {
    this.subsc.unsubscribe();
  }
}
// service
export class UserSubjectService {
private _user$ = new Subject<User>();  
get user$() {
    return this._user$.asObservable();
  }
  // 値を流す
  next(user: User) {
    return this._user$.next(user);
  }
}

export interface User {
  userId: string;
  name: string;
  email: string;
}

pipeの種類とかも紹介したいところですが長くなったのでまた別の機会に。