RxJSを活用した
Angularアプリの状態管理設計
状態管理
- 状態をどう管理するかはJS界隈でよく議論になる
- Fluxアーキテクチャを各FWで実装してる状況
- The Elm Architectureすげ〜
状態って?
- ユーザーがフォームに入力した文字列
- モーダルが開いている・閉じている
- ユーザーが開いているURL
- サーバーサイドから取得したJSON
- などなど??
Savkinの状態管理理論
と勝手に呼んでいる記事
Managing State in Angular Applications
https://blog.nrwl.io/managing-state-in-angular-applications-22b75ef5625f
Savkin先生
先生曰く
(記事の要約)
- フロントエンドの状態を管理するのは難しい
- 状態を6つに分類する
- 状態のうち、他と同期すべきものがいくつかある
- その実装をミスってバグを生むことが多い
- Reduxを使うと良い
- NgRxも良い
- 今回は状態の分類だけご紹介
状態を6つに分類する
- Server state
- Persistent state
- URL state
- Client state
- Transient client state
- Local UI state
Server State
- バックエンドのサーバーが保持する状態
- REST APIを通じて取得することが多い
Persistent state
- クライアントサイドに保存したServer state
- Server stateのキャッシュとも言える
- より良いUIのために楽観的更新をここで担ったりする
URL state
- ユーザーが現在開いているURL
Client state
- サーバーには保存しない状態
- 例: Todoリストの未完了だけ表示するフィルタ
- Client stateをURLに反映するのは良いプラクティス
- adwd-todo.com/user123/todos?status=done
Transient client state
- URLには反映しないクライアントの状態
- 例: YouTubeの再生時間
- URLには表示されないが、次に同じ動画を開くとその再生時間から再開する
- URLには反映されないので、そのURLを他人が開くと最初から再生する
- Cookieでやってるっぽい
Local UI State
- ボタンの色
- メニューが展開されているか
- など
状態の同期
- 6種類の状態のうち同期すべきものがある
- Server state <=> Persistent state
- URL state <=> Client state
- (Persistent state <=> URL state)
NgRx
NgRx
- Reduxインスパイア の状態管理ライブラリ
- store, effects, router-storeなど
- ちょっと前にバージョンが2.xから4.xに飛んだ
- Angularのバージョンと合わせるため?
- Angularチームのコミットも多く半公式?
- おそらくはAngularで大規模なアプリを作る場合はこれを使うのがセオリーになっていきそう
NgRxを使わない理由
(個人的な)
- ボイラープレートが多すぎる
- リッチにアプリケーションを書けるAngularにReduxの素朴なスタイルが合わない気がする
- Reduxの非同期・副作用を隔離するという動機もAngularではあまり意味がないと思う
- 個人的には1年以上React/Reduxやってたので、ものすごく飽きている 🙄
- Reduxスタイルではやりにくいケースも
- 実際にNgRxをちゃんと使ったわけではないので間違った判断かもしれない
どう状態管理するか?
- Savkin理論をベースに考える
- Persistent stateにはBehaviorSubjectが使える
- このアイデアはいくつか既存のものがある
- Angular Model Pattern
- Observable Data Service
- RxJS Real World (ng-japan 2017)
- このアイデアはいくつか既存のものがある
Persistent stateの実装
- バックエンドが返したJSONをストリームするBehaviorSubjectをつくる
BehaviorSubject
ComponentA
ComponentB
GET /users
PUT /users/123
DELETE /users/456
@Injectable()
export class UserHttpService {
private _user: BehaviorSubject<User>;
private user: Observable<User>;
constructor(private http: HttpClient) { }
fetchUser(): Observable<User> {
return this.http.get<User>('/api/user')
.switchMap((user: User) => {
if (this._user) {
this._user.next(user);
return this.user;
} else {
this._user = new BehaviorSubject(user);
this.user = this._user.asObservable();
return this.user;
}
});
}
getUser(): Observable<User> {
return this.user || this.fetchUser();
}
updateUser(user: Partial<User>) {
return this.http.put('/api/corporate', user)
.do(() => {
if (this._user) {
const current = this._user.getValue();
this._user.next({ ...current, ...user });
}
});
}
}
データをキャッシュしマルチキャストする
BehaviorSubject
外側からいじられたくないのでObservableに変換
HTTPから得たデータの後続にswitchMapでBehaviorSubjectをくっつけて更新が流れてくるようにする
既にfetch済みならそれでいいですというユースケースに対応
getValue()をつかって楽観的更新
URL, Client state
- URL state, Client stateも同期すべき
- URLをstateのsourceとする
- 変数でClient stateを表現しない
// inputにidを入力するとURLが/foo?id=123のように変わり、
// そのidのデータを取得して表示するコンポーネント
@Component({
template: `
<input type="text" #input placeholder="enter id"/>
<button (click)="onClick(input.value)">submit</button>
<p>{{result$ | async}}<p>`
})
export class FooComponent implements OnInit {
result$: Observable<string>;
constructor(
private route: ActivatedRoute,
private router: Router,
private service: FooService,
) { }
ngOnInit() {
// URLを状態のソースとして、変更を監視する
this.result$ = this.route.queryParams.switchMap((params: Params) =>
this.service.getFoo(params['id'])
);
}
onClick(id: string) {
// 状態の変更はURLに対して行う
this.router.navigateByUrl(`/foo?id=${id}`);
}
}
アーキテクチャ概要
Persistent state
Server state
Transient client state
Cookie, LocalStorage, ..
Client state
URL state
Service
Component
同期
同期
同期
リソースを管理してSubjectで公開する
ActivatedRoute, Routerを使ってURLに状態を反映・監視
Local UI state
リソースと画面の状態からあれこれ計算する
ドメインロジック
表示に専念する
難しいことは下のServiceに任せる
アーキテクチャ概要
Persistent state
Server state
Transient client state
Cookie, LocalStorage, ..
Client state
URL state
Service
Component
同期
同期
同期
Local UI state
Presentation
Domain logic
Resource management
アーキテクチャ概要
- Savkin状態理論に基づき状態を分類
- RxJSを使って状態を同期
- Server <=> Persistent state
- BehaviorSubjectを使う
- URL <=> Client state
- URLに状態の変化を反映する
- URLの変更を監視する
- (Persistent <=> URL state)
- URLの変更をトリガにPersistentの状態を更新
- Server <=> Persistent state
アーキテクチャ概要
- 関心に基づき階層化
- Component (Local UI state)
- 表示に集中
- 下位のサービスからデータを受け取る・イベントを渡す
- DomainService
- コンポーネントが扱うドメインロジックが集中、一番複雑
- ここに集めてテストしやすくなるはず?
- ComponentやHttpも登場しないので、Angularに非依存のIsolated Testができるはず
- コンポーネントが扱うドメインロジックが集中、一番複雑
- Resources (Persistent, Transient client state)
- リソースの管理に集中
- Component (Local UI state)
まとめ
- Angularでの状態管理
- Savkin先生の状態の分類
- Angular Model Pattern, Observable data serviceなどと呼ばれているSubject活用パターン
- Savkin理論+Subject活用でNgRx使わなくてもいけそう?
[簡易版]RxJSを活用したAngularアプリの状態管理設計
By adwd
[簡易版]RxJSを活用したAngularアプリの状態管理設計
Angular state-management with RxJS
- 2,885