Angularにおける状態管理をRxJSで試した
状態管理はフロントエンドで必要な機能としてよく取り上げられています。
ReactにおけるReduxが有名で、Reduxライクな状態管理のライブラリが各種フレームワークで用意されていて、それらを使うのが主流なようです。
AngularでもReduxライクなngrxというライブラリがあります。
ただ自分はReduxの経験がないので、パッと見で複雑そうな印象がありました。
ngrx以外の手段で状態管理を試してみました。
どのように状態管理を行うか
参考にしたのはこちらです。 slides.com coryrylan.com
「RxJSを活用したAngularアプリの状態管理設計」では状態の同期を取るという方法が紹介されています。
これを参考にして、今回はPersistent stateとServer stateを同期しつつ、stateをComponentで扱うことを試します。
stateの同期にはRxJSの機能であるBehaviorSubjectを利用することが紹介されています。
BehaviorSubjectを使えば、任意のタイミングでデータを流すことができてかつ、流れたデータを保持することが可能です。
実装
Angularの公式チュートリアルでおなじみのTour of Heroesを元にしました。
github.com
hero.service.ts
を変更して、Server stateと同期するPersistent stateを実装しました。
hero.service.ts
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, throwError as observableThrowError, BehaviorSubject, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { Hero } from './hero'; @Injectable() export class HeroService { private _heroes: BehaviorSubject<Hero[]>; private heroes: Observable<Hero[]>; private heroesUrl = 'app/heroes'; // URL to web api constructor(private http: HttpClient) { this._heroes = new BehaviorSubject([]); } fetchHeroes(): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) .pipe( switchMap(data => { this._heroes.next(data); this.heroes = this._heroes.asObservable(); return this.heroes; }) ); } getHeroes(): Observable<Hero[]> { return this.heroes || this.fetchHeroes(); } getHero(id: number): Observable<Hero> { return this.getHeroes().pipe( map(heroes => heroes.find(hero => hero.id === id)) ); } save(hero: Hero) { if (hero.id) { return this.put(hero); } return this.post(hero); } delete(hero: Hero) { const url = `${this.heroesUrl}/${hero.id}`; return this.http.delete<Hero>(url) .pipe( tap(() => { const current = this._heroes.getValue().filter(h => h !== hero); this._heroes.next(current); }), catchError(this.handleError) ); } // Add new Hero private post(hero: Hero) { return this.http .post<Hero>(this.heroesUrl, hero) .pipe( tap((h) => { const current = this._heroes.getValue(); this._heroes.next([...current, h]); }), catchError(this.handleError) ); } // Update existing Hero private put(hero: Hero) { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put<Hero>(url, hero) .pipe( tap(() => { const current = this._heroes.getValue() .map(h => { if (h.id === hero.id) { h = hero; } return h; }); this._heroes.next(current); }), catchError(this.handleError) ); } private handleError(res: HttpErrorResponse | any) { console.error(res.error || res.body.error); return observableThrowError(res.error || 'Server error'); } }
CRUDに当たる各メソッドでBehaviorSubjectのオブジェクトである_heroes
プロパティにnextメソッドでデータを流しています。
Serverへリクエストを発行するとともにそのデータを_heroes
に保持することで、Server stateとPersistent stateを同期しています。
また、BehaviorSubjectではnextメソッドを実行したタイミングでsubscribeメソッドが呼ばれるので、ComponentからPersistent stateを参照するのも簡単です。
heroes.component.ts
... ngOnInit(): void { this.getHeroes(); } getHeroes(): void { this.heroService .getHeroes() .subscribe( heroes => this.heroes = heroes, error => this.error = error ) } deleteHero(hero: Hero, event: any): void { event.stopPropagation(); this.heroService.delete(hero).subscribe(res => { if (this.selectedHero === hero) { this.selectedHero = null; } }, error => (this.error = error)); } ...
BehaviorSubjectのsubscribeメソッドが実行される流れです。
- データを削除するためにComponentクラスのdeleteHeroメソッドが呼ばれる
- HeroServiceクラスのdeleteメソッドが呼ばれる
- BehaviorSubjectのnextメソッドが呼ばれる
- Componentクラスで定義した
this.heroService.getHeroes().subscribe
の処理が呼ばれる
結果的に削除後のデータがBehaviorSubjectによってComponentクラスのheroes
プロパティからも参照されます。
まとめ
BehaviorSubjectが有能でした。
BehaviorSubjectが保持するデータとServerのデータを同期するようにServiceクラスを作り、ComponentクラスはそのBehaviorSubjectからのみデータを取得するように設計すれば、単方向のデータフローが実現できて、シンプルな状態管理を実現できそうです。