AngularのBehaviorSubjectでコンポーネント間の情報を共有する

Angular で コンポーネント間で情報を共有する

コンポーネント間で情報共有ですが、ReactならReduxやMobXなどがあり、VueならVuexがあり、Storeを作成することでどのコンポーネントからも共通の情報にアクセスすることが可能です。

Angular にはデフォルトではそういったFlux機能は提供されておらず、コンポーネント間で情報を共有するには RxJS のBehaviorSubjectを利用する方法がよく利用されます。

Subjectとは?

そもそもRxJSのSubjectとはなにかから解説します。RxJSのSubjectは Observerとしても Observable としても動くクラスです。

ObserverとObservable

通常、RxJSではObservableクラスを通して、ストリームを購読できるObservableとストリームに値を流すことができるObserverを作成することができます。

下記のコードではcountObservableを作成してngOnInit内で購読(subscribe)してcountの更新を行っています。
new Observableの際に引数として指定したobserverでは1秒ごとcountアップした値をcountObservableにストリーミングしています。

import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs/Rx";
   
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  public count = 0;
    
  // Observableを作成
  private countObservable = new Observable<number>(observer => {
    let _count = 0;
    setInterval(() => {
      _count++;
      // observerを通して購読されたObservableに値を伝える
      observer.next(_count);
    }, 1000);
  });
    
  ngOnInit() {
    // Observableを購読
    this.countObservable.subscribe(count => {
      this.count = count;
    });
  }
}

Edit Angular

このように値を値の購読をObservableが、値のストリーミングをObserverが担っています。

Subjectとは?

ObserverとObservableの性質をもつSubjectは、作成されたインスタンス単体で値の購読も値のストリーミングも可能なクラスです。

下記のサンプルではpublicなcountというプロパティとprivateな_countというプロパティを作成して、countSubjectというSubjectを作成してngOnInit内で購読処理を行いpublicなcountを更新しています。

increment()内ではprivateな_countを加算してcountSubjectにストリーミングしています。

import { Component, OnInit } from "@angular/core";
import { Subject } from "rxjs/Rx";
  
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  public count = 0;
  private _count = 0;
  
  // Subjectを作成
  private countSubject = new Subject<number>();
  
  ngOnInit() {
    // Subjectを更新
    this.countSubject.subscribe(count => {
      this.count = count;
    });
  }
  
  public increment() {
    this._count++;
    // Subjectに値を流す
    this.countSubject.next(this._count);
  }
}

Edit Angular

BehaviorSubjectとは?

さて、Subjectは値を購読する、値を流すの機能がありましたが、BehaviorSubjectは流れてきた値を保持することが可能です。

BehaviorSubjectではインスタンス生成時の引数に値の初期値を、設定でき.getValueメソッドで現在の値を取得することができます。

import { Component, OnInit } from "@angular/core";
import { BehaviorSubject } from "rxjs/Rx";
  
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  public count: number;
  
  // Subjectを作成、値の初期値は0
  private countSubject = new BehaviorSubject<number>(0);
  
  ngOnInit() {
    // Subjectを購読
    this.countSubject.subscribe(count => {
      this.count = count;
    });
  }
  
  public increment() {
    // Subjectから現在の値を取得
    const count = this.countSubject.getValue();
    // 1加算した値をストリーミングする
    this.countSubject.next(count + 1);
  }
}

Edit Angular

BehaviorSubjectは値を保持する特性からコンポーネント間のデータ共有に利用することができます。

ServiceとしてBehaviorSubjectを分離

コンポーネント間のデータ共有に利用するためにはまずBehaviorSubjectをServiceとして分離します。

app.service.ts を以下のようなスクリプトで追加します。先程のBehaviorSubjectのサンプルから初期値の設定とincrement() を分離したものとです。

import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs/Rx";
  
@Injectable()
export class AppService {
  public countSubject = new BehaviorSubject<number>(0);
  
  increment() {
    // Subjectから現在の値を取得
    const count = this.countSubject.getValue();
    // 1加算した値をストリーミングする
    this.countSubject.next(count + 1);
  }
}

app.component.tsでは次のようにserviceより受け取ったSubjectを購読してserviceを経由して値のアップデートを行っています。
Service内の値は共有されるためどこかのコンポーネントでアップデートした値はすべてのコンポーネントに共有されます。

import { Component, OnInit } from "@angular/core";
import { BehaviorSubject } from "rxjs/Rx";
  
import { AppService } from "./app.service";
  
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  public count: number;
  
  constructor(private appService: AppService) {}
  
  ngOnInit() {
    // ServiceのSubjectを購読
    this.appService.countSubject.subscribe(count => {
      this.count = count;
    });
  }
  increment() {
    // Serviceのincrementを実行
    this.appService.increment();
  }
}

Edit Angular

購読を停止する(1箇所)

ServiceのSubjectを購読するさいに忘れがちなのが購読を停止です。コンポーネントが破棄されてもsubscribeイベントは破棄されませんのでメモリリークの原因になってしまいます。

購読を停止はsubscribe()を実行した際の返り値であるsubscriptionオブジェクトのunsubscribe()を実行することで購読の停止が可能ですのでngOnDestroy()などに購読停止処理を仕込んでおきましょう。


// 購読時の処理
this.subscription = this.appService.countSubject.subscribe(count => {
    this.count = count;
});
  
// 購読停止処理
this.subscription.unsubscribe();

購読を停止する(複数箇所)

コンポーネント内でsubscribe()を複数箇所で行っている場合はunsubscribe()するのが手間がかかります。

そいった場合はRxJSのSubscriptionクラスを利用しましょう。Subscriptionクラスから作成したインスタンスは.add()でsubscriptionオブジェクトを追加していきunsubscribe()でまとめて購読を停止することが可能です。

import { Subscription } from 'rxjs/Subscription';
  
export class AppComponent implements OnInit, OnDestroy {
  // Subscriptionクラスからインスタンスsubscriptionsを作成
  private subscriptions = new Subscription();
  
  ngOnInit() {
    // subscriptionオブジェクトを追加
    this.subscriptions.add(
      this.appService.countSubject.subscribe(count => {
        this.count = count;
      })
    );
 }
  
  public ngOnDestroy() {
    // 購読停止処理
    this.subscriptions.unsubscribe();
  }
}

asyncパイプを利用する

購読や購読の停止について解説をしてきましたが、AngularにはBehaviorSubjectなどのRxJSのストリームをそのまま表示するasyncパイプが存在します。

先程のServiceとしてBehaviorSubjectを分離したコードをasyncパイプを利用して書き直してみます。

app.component.tsでは値の購読や代入などの処理を省略しています。

import { Component, OnInit } from "@angular/core";
import { BehaviorSubject } from "rxjs/Rx";
  
import { AppService } from "./app.service";
  
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  constructor(private appService: AppService) {}
  
  increment() {
    // Serviceのincrementを実行
    this.appService.increment();
  }
}

app.component.htmlは次のようにappService.countSubjectを直接読み込みasyncパイプを記述しています。

<div style="text-align:center">
	<h1>{{appService.countSubject | async}}</h1>
	<button (click)="increment()">increment</button>
</div>

asyncパイプを利用すればコンポーネントの破棄時にわざわざ購読停止をしなくてもよくコードの見通しが良くなりますが、値の加工などがRxJSに依存するため少しめんどくさくなるデメリットがあります。

Edit Angular

BehaviorSubjectはAngularを利用する際の重要なテクニックなので覚えておいてください。

スポンサードリンク

«会社設立時の物件探し | メイン | 株式会社トゥーアールの2018年を振り返る»