AngularのBehaviorSubjectでコンポーネント間の情報を共有する
コンポーネント間で情報共有ですが、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;
});
}
}
このように値を値の購読を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);
}
}
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);
}
}
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();
}
}
購読を停止する(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に依存するため少しめんどくさくなるデメリットがあります。
BehaviorSubjectはAngularを利用する際の重要なテクニックなので覚えておいてください。