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 の用意
- constructor で渡した値が initial state になる
- 値を渡さないと ComponentStore が初期化されない(後述の
setState()
を使って初期化することもできる)
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
setState()
は引数に projector または state を渡すことができるprojector(関数) が渡された場合は、内部で
updater()
が呼び出されるstate が渡された場合は ComponentStore の constructor と同様に initState が呼び出される
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 にまかせてもよさそうだと感じた。