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

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

@ngrx/signals を使う

はじめに

Angular Signals の API が公開されてからある程度の時間が経過したが、signals によりアプリケーションの store の実装もアップデートが必要になってきた。

Angular アプリケーションで store 管理をするときに、よく選ばれる NgRx からも signals 用の @ngrx/signals というライブラリが公開されているので、今回はこちらを触ってみた。

SignalStore を状態管理

store class の作成

公式ページにも注釈がある通り 2024/6 現在 developer preview であるため、破壊的な変更が入る可能性はまだ多いにある。 メインとなる機能は 4 つあるが今回は SignalStore を使ってみた。

まずは store を作成する必要がある。作成には signalStore() を使う。

signalStore() は最低 1 つの引数を取る。一番最小の形は withState() を使って initialState を渡すパターンである。

type UsersState = {
  users: User[];
  isLoading: boolean;
};

const initialState: UsersState = {
  users: [],
  isLoading: false,
};

export const UserStore = signalStore(withState(initialState));

これだけで store が用意できる。signalStore() は Injectable な class を返すため、使うには任意の component のインジェクターに登録する必要がある。

@Component({
  ...
  providers: [UserStore],
})
export class CounterComponent {
  private readonly store = inject(UserStore);
  readonly users = computed(() => this.store.users());
}

withState() を使うと initialState の各プロパティに対応した signals が自動的に生成される。今回の例だと this.store.usersSignal<User[]> として作られる。

また、上記の例では要素インジェクターに登録されることになるが、もし環境インジェクターに登録したい場合は signalStore() の第一引数に { providedIn: 'root' } を渡せば良い。第一引数が providedIn のプロパティを持つ object であれば、自動的にそれを SignalStoreConfig と見なし @Injectable() のパラメータとして使われる。

computed signals を store に定義

signalStore() の仕組みで computed signals を作ることもできる。これは withComputed() を使い以下のように記述する。

export const UserStore = signalStore(
  withState(initialState),
  withComputed(({ users }) => ({ userCount: computed(() => users().length) })),
);

すると users の長さを管理する userCount という Signal<number> の signal が用意される。

メソッドの追加

次に紹介するのはメソッドの追加である。signalStore()withMethods() を使ってメソッドを追加できる。withMethods() は inject context 内で実行される。そのため外部 service を inject することもできる。

export const UserStore = signalStore(
  withState(initialState),
  withComputed(({ users }) => ({ userCount: computed(() => users().length) })),
  withMethods((store, userService = inject(UserService)) => ({
    async initialize(): Promise<void> {
      patchState(store, { isLoading: true });
      const users = await firstValueFrom(userService.fetchAll());
      patchState(store, { users, isLoading: false });
    },
  })),
);

突然 patchState() が出てきたが、名前の通り state を更新する関数である。 上記の通り initialize() を定義し、これを component の ngOnInit() などで呼び出せば UserService の fetchAll() を call し、返り値を新しい state としてセットする処理を動かせる。

ベースとなるロジックはこれだけである。

まとめ

上記に書いたようにたったこれだけで、signals ベースの状態管理を始めることができる。 既存のアプリケーションを signals ベースに置き換えていく上で、状態管理をどうするか試行錯誤する時期だと思うが、3rd party ライブラリも徐々に signals に対応してきているので、まずは大きな肩に乗ってノウハウを貯めていきたい。