とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

NgRx ComponentStore を試す

NgRx の ComponentStore を試したのでメモ https://ngrx.io/guide/component-store

サンプルはこちら https://stackblitz.com/edit/angular-ngrx-component-store

@ngrx/component-store の特徴

  • @ngrx/store と違い component などの local な状態管理するのに役立つ
  • component に紐付けられるから component が破棄されるとクリーンアップされる

インストール

  • いつもどおり
$ yarn add @ngrx/component-store

サンプル

  • 簡単なカウンターアプリを作る

Store の用意

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

export interface CounterState {
  count: number;
}

@Injectable()
export class CounterStore extends ComponentStore<CounterState> {
  constructor() {
    super({ count: 0 });
  }
}

Read state

  • select() という名前の selector が提供されているのでそれを使うと簡単に read できる
  • select() は state を map して返すので引数に projector を指定することで好きな形で取得が可能
  import { Injectable } from '@angular/core';
  import { ComponentStore } from '@ngrx/component-store';
+ import { Observable } from 'rxjs';

  export interface CounterState {
    count: number;
  }

  @Injectable()
  export class CounterStore extends ComponentStore<CounterState> {
    constructor() {
      super({ count: 0 });
    }

+   readonly count$: Observable<number> = this.select((state) => state.count);
  }
  • この store を使用する component 側は以下のように書くだけでよい
import { Component } from '@angular/core';
import { CounterStore } from './counter.store';

@Component({
  selector: 'my-app',
  template: `
    <div>This count is {{ count$ | async }}</div>
  `,
  styleUrls: ['./app.component.css'],
  providers: [CounterStore],
})
export class AppComponent  {
  constructor(private readonly counterStore: CounterStore) {}
  readonly count$ = this.counterStore.count$;
}
  • Injectable な class は Injector の範囲内でシングルトンなので component ごとに providers に登録すると component ごとの状態が持てる

Update state

  • state を更新するには updater() または setState() を使う必要がある

updater

  • updater() は現在の state を引数に更新された state を返す
  import { Injectable } from '@angular/core';
  import { ComponentStore } from '@ngrx/component-store';
  import { Observable } from 'rxjs';

  export interface CounterState {
    count: number;
  }

  @Injectable()
  export class CounterStore extends ComponentStore<CounterState> {
    constructor() {
      super({ count: 0 });
    }

    readonly count$: Observable<number> = this.select((state) => state.count);

+   readonly add = this.updater((state) => ({ count: state.count + 1 }));
}
  import { Component } from '@angular/core';
  import { CounterStore } from './counter.store';

  @Component({
    selector: 'my-app',
    template: `
      <div>This count is {{ count$ | async }}</div>
+     <button (click)="onClickAddButton()">+1</button>
    `,
    styleUrls: ['./app.component.css'],
    providers: [CounterStore],
  })
  export class AppComponent  {
    constructor(private readonly counterStore: CounterStore) {}
    readonly count$ = this.counterStore.count$;
  
+   onClickAddButton() {
+     this.counterStore.add();
+   }
}

setState

  import { Injectable } from '@angular/core';
  import { ComponentStore } from '@ngrx/component-store';
  import { Observable } from 'rxjs';

  export interface CounterState {
    count: number;
  }

  @Injectable()
  export class CounterStore extends ComponentStore<CounterState> {
    constructor() {
-     super({ count: 0 });
+     super();
+     this.setState({ count: 0 });
    }

    readonly count$: Observable<number> = this.select((state) => state.count);

-     readonly add = this.updater((state) => ({ count: state.count + 1 }));
+     readonly add = () => this.setState((state) => ({ count: state.count + 1 }));
  }

所感

component に閉じたローカルな状態管理は ComponentStore にまかせてもよさそうだと感じた。