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をちゃんと使ったわけではないので間違った判断かもしれない

どう状態管理するか?

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の状態を更新

アーキテクチャ概要

  • 関心に基づき階層化
    • Component (Local UI state)
      • 表示に集中
      • 下位のサービスからデータを受け取る・イベントを渡す
    • DomainService
      • コンポーネントが扱うドメインロジックが集中、一番複雑
        • ここに集めてテストしやすくなるはず?
        • ComponentやHttpも登場しないので、Angularに非依存のIsolated Testができるはず
    • Resources (Persistent, Transient client state)
      • リソースの管理に集中

まとめ

  • Angularでの状態管理
  • Savkin先生の状態の分類
  • Angular Model Pattern, Observable data serviceなどと呼ばれているSubject活用パターン
  • Savkin理論+Subject活用でNgRx使わなくてもいけそう?
Made with Slides.com