本記事は、Angular Advent Calendar 2020 21日目の記事です。
昨今、SPAとは切っても切り離せない概念となりつつある「状態管理」ですが、初心者にとってはその概念がまず難しいものです。特にAngularはフレームワークレベルでRxJSと密結合になっており、RxJSが分からなければ状態管理すらできないという、大変初心者に厳しい仕様となっています。
しかし、AngularとRxJS特有の「クセ」さえ理解してしまえば、すっきりとしたエレガントなコードで機能を実装することができます。
ここでは、簡単なToDoアプリを題材として、Angularにおける状態管理のやり方と、各種ライブラリの特徴を包括的に紹介していきたいと思います。
この記事の読者想定
- Angularチュートリアルは一通りやってみたけど、やっぱりrxjsの概念が分からないよ…という方
- 状態管理したいけど、どうやってやればいいのかわからないという方
- ライブラリがありすぎてどれ選べばいいのかわからないという方
RxJSの概念を知ろう
状態管理というテーマからすると前書き的な存在だと思いますが、分からない方もいらっしゃると思いますので、ここで軽くRxJSが何をやっているのかをご説明したいと思います。
RxJSは、Observable
というクラスを中心として、状態を保持したりリアクティブに状態を配信したりするためのクラス・関数が入ったJavascriptライブラリです。この「リアクティブ」にというのが重要で、Angularにおける各種状態管理ライブラリの根幹を担っているといってもいいでしょう。
ここでは簡単に、BehaviorSubject
と、Observable
についてのみ紹介しておきます。
まずObservable
は、「あるデータソースに流れてくる値を購読(Subscribe)する」という役割を担うクラスです。上記の図の場合、データソースはBehaviorSubject
にあたり、初期状態はBehaviorSubjectに{"hoge": true}
が設定されていますが、コンポーネント上のボタンなどで更新イベントが発行されると、BehaviorSubject.next()
によってデータソースの値が{"hoge": false}
に変更されます。すると、購読しているComponentにも変更された値である{"hoge": false}
が流れてきます。
一方でBehaviorSubject
は、「Observableに対し値を流すためのデータソース」の役割を担うクラスです。外部からのnext関数の呼び出しにより値を更新することができ、更新された値はSubscribeされているObservable
すべてにリアルタイムで配信されます。同様の働きをするクラスにSubject
がありますが、BehaviorSubject
が「最後に更新された値を保持する」のに対し、Subject
は保持しません。また、Angularにおいて親子コンポーネント間のデータのやり取りでよく使われるEventEmitter
はSubject
とほぼ同様のものです。
これを簡単にソースコードで表すと、大体以下のようになります。
<pre><code>{{state$ | async | json}}</code></pre> <!-- asyncパイプを使うことにより購読 -->
<button (click)="setState({ hoge: true })">true</button>
<button (click)="setState({ hoge: false })">false</button>
import { RxjsExampleService } from './rxjs-example.service';
@Component({
selector: 'app-rxjs-example',
templateUrl: './rxjs-example.component.html',
styleUrls: ['./rxjs-example.component.scss'],
})
export class RxjsExampleComponent implements OnInit {
state$ = this.rxjsExampleService.state$.asObservable(); // Observableとして読み取り専用で参照する
constructor(private readonly rxjsExampleService: RxjsExampleService) {}
ngOnInit(): void {
this.state$.subscribe((state) => console.log(state)); // 購読
}
setState(state: { hoge: boolean }): void {
this.rxjsExampleService.state$.next(state); // BehaviorSubjectに新しい値を設定する
}
}
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class RxjsExampleService {
constructor() {}
state$ = new BehaviorSubject<{ hoge: boolean }>({ hoge: true });
}
<button>
をクリックすると、リアクティブに値が変化することが確認できます。
サンプルはこちら。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/rxjs-example
状態管理する
さて、簡単にRxJSの概念のさわりをご紹介したところで、Angularで状態管理を行う方法を何個かご紹介していきたいと思います。
BehaviorSubject
概要
先ほどBehaviorSubject
は「最後に更新された値を保持する」と紹介しましたが、この性質を利用して簡易的なストアとして利用することができます。
完全にストア用のサービスクラスを作る必要はなく、既存のサービスクラスにストアの機能を付け足すことも可能で、@Injectable({providedIn: 'root'})
として指定すればグローバルストアとして、@Component({providers: [SomeState]})
として指定すればコンポーネントストアになります。
長所
- コンポーネントストア/グローバルストア両方で使える
- 追加のライブラリインストールが必要ない
- シンプル
短所
- あくまで簡易的なものであることに留意しなければならない
- シンプルがゆえに凝ったことをしようとするとコード量が増える
実装サンプル
サンプルはこちら。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/rxjs-state
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Todo } from '../types/todo';
import { v4 as uuid } from 'uuid';
@Injectable({
providedIn: 'root',
})
export class RxjsStateService {
private _todos$: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([
{
id: uuid(),
title: 'あ',
done: true,
},
{
id: uuid(),
title: 'い',
done: false,
},
]);
todos$: Observable<Todo[]> = this._todos$.asObservable();
constructor() {}
add(todo: Todo): void {
const current = this._todos$.getValue();
this._todos$.next([...current, todo]);
}
remove(todo: Todo): void {
const current = this._todos$.getValue();
const removed = current.filter((o) => o.id !== todo.id);
this._todos$.next(removed);
}
markAsDone(todo: Todo): void {
const current = this._todos$.getValue();
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = true;
this._todos$.next([...current]);
}
markAsUndone(todo: Todo): void {
const current = this._todos$.getValue();
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = false;
this._todos$.next([...current]);
}
}
private _todos$: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([
...
]);
todos$: Observable<Todo[]> = this._todos$.asObservable();
この部分がストアのようなふるまいをします。
asObservable()
とすることで、購読専用のObservable
クラスに変換してくれます。このObservable
を購読した場合は、this._todos$.next()
を実行するたびに、リアクティブに値が変わります。
逆にリアクティブではないスナップショットが必要な場合は、getValue()
を用います。
RxJSの基本機能のみを使用して実装しているので、シンプルかつビルドサイズも軽量化できるのですが、大規模開発や複数人開発などで用いるとコードの書き方に揺らぎが生じるので、事前にコード規約をしっかり定めておいたほうがよさそうです。
また、BehaviorSubject
が提供するのはあくまでも「最後に入力された値を保持し、購読者に流す」機能だけなので、追加、更新、削除といったありとあらゆる便利機能はすべて手書きしなければなりません。
import { Todo } from '../types/todo';
@Injectable()
export class RxjsStateComponentService {
private _list$: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
list$: Observable<Todo[]> = this._list$.asObservable();
set list(todos: Todo[]) {
this._list$.next(todos);
}
private _onlyViewNotDone$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
false
);
onlyViewNotDone$: Observable<boolean> = this._onlyViewNotDone$.asObservable();
set onlyViewNotDone(bool: boolean) {
this._onlyViewNotDone$.next(bool);
}
constructor() {}
}
こちらはコンポーネントストアです。
ほぼ同じような構成になっていますが、単純に「表示したいリスト」を保存するだけの役割なので、関数ではなくsetterで値を更新できるようにしています。
グローバルストアとコンポーネントストアでほぼ同じ書き方ができる、というのは場合によっては利点かもしれません。
NgRx
概要
Angularにおける状態管理ライブラリの選定で最初に選択肢に上がるのがこのNgRxではないかと思います。状態管理系の紹介記事等もNgRxが圧倒的に情報量が多く、デファクトスタンダードと化している感があります。
グローバルストアに用いることが多いですが、最近コンポーネントストア用の拡張が入りましたので、オールラウンドに用いることができます。
ただ、Reduxを意識して開発されている関係上、かなり複雑なライブラリとなっており、学習難易度が高いのが難点です。
長所
- グローバルストア/コンポーネントストア両方で使える
- Redux・Vuexの使用経験があれば、比較的理解しやすい
- 厳格な設計なので、実装パターンに迷うことがない
- Redux devtoolsに対応していて、ストアの現在の状況をブラウザで簡単に表示可能
短所
- いろんな要素全部入りのライブラリなので、使いこなすには苦労する+npm読みで1.31MBと重い
- ファイル数が多くなり見通しが悪くなりがち
- 学習難易度高し
NgRx
は用途によって複数のサブライブラリに分かれており、今回のサンプルアプリ実装ではコアライブラリである@ngrx/store
のほか、@ngrx/entity
、@ngrx/store-devtools
、@ngrx/component-store
、@ngrx/schematics
を用いています。
実装サンプル
サンプルはこちら。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/ngrx-state
import { Update } from '@ngrx/entity';
import { createAction, props } from '@ngrx/store';
import { Todo } from 'src/app/types/todo';
export const loadNgrxStates = createAction('[NgrxState] Load NgrxStates');
export const loadNgrxStatesSuccess = createAction(
'[NgrxState] Load NgrxStates Success',
props<{ data: any }>()
);
export const loadNgrxStatesFailure = createAction(
'[NgrxState] Load NgrxStates Failure',
props<{ error: any }>()
);
export const addTodo = createAction(
'[NgrxState] Add Todo',
props<{ todo: Todo }>()
);
export const updateTodo = createAction(
'[NgrxState] Update Todo',
props<{ todos: Update<Todo> }>()
);
export const removeTodo = createAction(
'[NgrxState] Remove Todo',
props<{ id: string }>()
);
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { createReducer, on } from '@ngrx/store';
import { Todo } from 'src/app/types/todo';
import { addTodo, removeTodo, updateTodo } from './ngrx-state.actions';
export interface State extends EntityState<Todo> {}
export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();
export const initialState: State = adapter.getInitialState();
export const reducer = createReducer(
initialState,
on(addTodo, (state, { todo }) => {
return adapter.addOne(todo, state);
}),
on(updateTodo, (state, { todos }) => {
return adapter.updateOne(todos, state);
}),
on(removeTodo, (state, { id }) => {
return adapter.removeOne(id, state);
})
);
const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = adapter.getSelectors();
export const selectTodoIds = selectIds;
export const selectTodoEntities = selectEntities;
export const selectAllTodos = selectAll;
export const selectTodoTotal = selectTotal;
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ngrxStateFeatureKey } from '.';
import * as fromNgrxState from './ngrx-state.reducer';
const getState = createFeatureSelector<fromNgrxState.State>(
ngrxStateFeatureKey
);
export const selectAllTodos = createSelector(
getState,
fromNgrxState.selectAllTodos
);
ストアの書き方は完全にRedux/Vuexに近似しています。Actionsでストアの値に対して行いたい動作を定義し、Reducerでアクションごとの実際の挙動を実装し、Selectorでストアから値を取得するためのハンドラを実装する、というのが基本的な流れです。
かっちりと書き方が定められているため、誰が書いても同じコードになることはまあ利点といえるかもしれませんが、その代わりファイル数が多くなり見通しが悪く、内部が完全にブラックボックス化されているような感じなので、Fluxアーキテクチャを理解していない人間が見ると何をやっているのかわからないのが難点です。
もちろんこのすべてを手で書く必要はなく、テンプレートを自動生成するためのschematicsは用意されていますので、比較的手数は少ないです。
また、NgRx v10からコンポーネントストア向けの@ngrx/component-store
というサブライブラリが追加されています。
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Todo } from 'src/app/types/todo';
type State = {
list: Todo[];
onlyViewNotDone: boolean;
};
@Injectable()
export class NgrxStateComponentStore extends ComponentStore<State> {
readonly list$ = this.select((state) => state.list);
readonly onlyViewNotDone$ = this.select((state) => state.onlyViewNotDone);
readonly setList = this.updater((state, todos: Todo[]) => ({
...state,
list: todos,
}));
readonly setOnlyViewNotDone = this.updater((state, bool: boolean) => ({
...state,
onlyViewNotDone: bool,
}));
constructor() {
super({ list: [], onlyViewNotDone: false });
}
}
BehaviorSubject
でこつこつやっていたコンポーネント単位の状態管理を、ストアらしく実装できます。Fluxの流儀からは外れていて、後述のAkitaの書き方に近いものになっています。
おまけとして、@ngrx/store-devtools
という、ブラウザ上でストアの現在の状況を確認するための拡張機能へ接続するためのライブラリがあります。
詳細な使い方はここでは省きますが、@ngrx/store-devtools
をインストールし、Redux devtools
というブラウザの拡張機能をインストールすることで、現在のストアの状況を確認することができます。これがあるとないとではデバッグの難易度が大違いなので、ぜひ導入することをお勧めします。
Akita
概要
Store/Queryの2クラスで完結する、シンプルに状態管理を行うことのできるライブラリです。
グローバルストア/コンポーネントストア両方に対応しており、またRedux Devtools用の拡張ライブラリもあります。
NgRxに比べると知名度が低く、npmの週間ダウンロード数はNgRxの10分の1と大差をつけられています。紹介記事などは少なめですが、公式ドキュメントが充実しており、それを見るだけで大抵のことはわかります。
よりAngularの機能に寄り添った設計がされていて、代表的なところでは「Angular Routerの履歴の状態管理」「ReactiveFormの状態管理」「LocalStorageを用いたストアの永続化」といった種々の機能/プラグインがあります。
長所
- グローバルストア/コンポーネントストア両方で使える
- 基本的な状態管理だけなら2クラスで完結するシンプルさ
- 設計の適度な緩さ
- 追加機能/プラグインが豊富
- Redux devtoolsに対応していて、ストアの現在の状況をブラウザで簡単に表示可能
- アイコンの秋田犬がかわいい
短所
- npm読みで2.74MBと重い(NgRxと異なり、サブライブラリに分かれていないため)
- 圧倒的知名度の低さ
- 検索性が悪い(Akitaで検索すると秋田県が出てきます)
実装サンプル
サンプルはこちら。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/akita-state
import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { Todo } from 'src/app/types/todo';
export interface TodosState extends EntityState<Todo, string> {}
@StoreConfig({ name: 'todos' })
@Injectable({ providedIn: 'root' })
export class AkitaStateStore extends EntityStore<TodosState> {
constructor() {
super();
}
markAsDone(todo: Todo): void {
this.update(todo.id, (entity) => ({
...entity,
done: true,
}));
}
markAsUndone(todo: Todo): void {
this.update(todo.id, (entity) => ({
...entity,
done: false,
}));
}
}
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { AkitaStateStore, TodosState } from './akita-state.store';
@Injectable({ providedIn: 'root' })
export class AkitaStateQuery extends QueryEntity<TodosState> {
todos$ = this.selectAll();
constructor(protected store: AkitaStateStore) {
super(store);
}
}
ストアの機能を実装するために必要なクラスは上記の2つだけとなっていて、とてもシンプルです。
EntityStore
というクラスにはすでにコレクションを操作するための各種便利関数が実装済みで、これを拡張するだけでストアとして利用できます。もちろんStore
のほうに独自で関数を実装してもいいですし、すべての便利関数はpublic関数として定義されているので、コンポーネントから直接使うこともできます。(これは賛否両論あると思いますので、チーム内で利用法について統一見解を作ることが大事だと思います)
クエリのほうもデータ取得に便利な関数が実装されたQueryEntity
を拡張しているだけです。
このような「シンプルさ」と、Angular初心者でも比較的とっつきやすい「適度な緩さ」がAkitaの特徴です。
NgRxが関数主体の設計になっている一方で、このAkitaがあくまでもクラス主体の設計であることは、人によっては「時代遅れ」と見られるかもしれませんが、AngularのServiceクラスとの親和性を考えると、非常に合理的な設計になっていると感じます。
import { Injectable } from '@angular/core';
import { guid, Query, Store } from '@datorama/akita';
type State = { onlyViewNotDone: boolean };
@Injectable()
export class AkitaStateComponentStore extends Store<State> {
constructor() {
super(
{
onlyViewNotDone: false,
},
{ name: `AkitaState-${guid()}` }
);
}
setOnlyViewNotDone(bool: boolean): void {
this.update({ onlyViewNotDone: bool });
}
}
@Injectable()
export class AkitaStateComponentQuery extends Query<State> {
onlyViewNotDone$ = this.select('onlyViewNotDone');
constructor(protected store: AkitaStateComponentStore) {
super(store);
}
}
コンポーネントストア側も、グローバルストアと全く同じ要領で2つのクラスを作るだけです。
@Injectable()
がrootではなく、インスタンスが複数生成されることになるため、super()
でストア名にユニークなIDを振っているところがグローバルストアとの差異です。
グローバル/コンポーネント間でインターフェースが同じなので、実装時に迷いが生じません。
また、AkitaにもNgRxと同様、現在のストアの状況を見ることができる@datorama/akita-ngdevtools
というプラグインがあります。
Angularの場合、ng add @datorama/akita
でインストールすることで、このdevtoolsを入れるかどうか聞いてくれます。Yesと答えた場合、インストールとapp.module.ts
への導入まで自動でやってくれますので大変便利です。
@rx-angular/state
概要
これまでのライブラリとはかなり毛色が違い、「リアクティブでよりスマートなコンポーネントストアの実装」を行うために開発されたライブラリです。その思想上、RxJSへの深い理解が必須条件となりますが、その代わりにAngularで最も煩雑なものの一つであるSubscription
の管理から完全に開放されるという、他に代替しがたい利点があります。
最近話題に上るようになったライブラリで、まだまだ発展途上ではありますが、Subscription
の管理フリーのほか、どうしても立ちはだかるObservable
周りのパフォーマンスチューニングが初期状態ですでに行われていたりなど、現状でもすでにかなり強力なライブラリに仕上がっています。
長所
- 小難しいコードなしでパフォーマンスの高いコンポーネントストアを作れる
- Subscriptionを自分で管理する必要なし(!)
-
@Input()
@Output()
といったイベントとの親和性も高い - npm読みで891KB
短所
- RxJSへの深い理解が必須
- グローバルストアとしても一応使えるが、使い勝手は悪い
- ストアの状態をブラウザ等で確認することはできない
実装サンプル
サンプルはこちら。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/rx-angular-state
コンポーネントストアメインのライブラリなので、先にコンポーネントストア側を紹介します。
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { RxState } from '@rx-angular/state';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { v4 as uuid } from 'uuid';
import { Todo } from '../types/todo';
import {
GLOBAL_RX_STATE,
RxAngularGlobalState,
} from './store/rx-angular-state.state';
interface State {
list: Todo[];
onlyViewNotDone: boolean;
}
@Component({
selector: 'app-rx-angular-state',
templateUrl: './rx-angular-state.component.html',
styleUrls: ['./rx-angular-state.component.scss'],
providers: [RxState],
})
export class RxAngularStateComponent implements OnInit {
readonly list$ = this.state.select('list');
readonly onlyViewNotDone$ = this.state.select('onlyViewNotDone');
form = this.fb.group({
title: [''],
});
constructor(
private readonly fb: FormBuilder,
@Inject(GLOBAL_RX_STATE)
private readonly globalState: RxState<RxAngularGlobalState>,
private readonly state: RxState<State>
) {
this.state.set({
list: [],
onlyViewNotDone: false,
});
this.state.connect(
'list',
combineLatest([
this.globalState.select('todos'),
this.onlyViewNotDone$,
]).pipe(
map(([todos, onlyViewNotDone]) => {
if (onlyViewNotDone) {
return todos.filter((o) => !o.done);
} else {
return todos;
}
})
)
);
}
ngOnInit(): void {}
onClickAdd(): void {
const current = this.globalState.get('todos') ?? [];
this.globalState.set({
todos: [
...current,
{
id: uuid(),
title: this.form.get('title').value,
done: false,
},
],
});
this.form.reset({
title: '',
});
}
onClickRemove(todo: Todo): void {
this.globalState.set({
todos: [
...this.globalState
.get('todos')
.filter((o) => o.id !== todo.id),
],
});
}
onClickMarkAsDone(todo: Todo): void {
const current = this.globalState.get('todos') ?? [];
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = true;
this.globalState.set({
todos: [...current],
});
}
onClickMarkAsUndone(todo: Todo): void {
const current = this.globalState.get('todos') ?? [];
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = false;
this.globalState.set({
todos: [...current],
});
}
onChangeOnlyViewNotDone(bool: boolean): void {
this.state.set({ onlyViewNotDone: bool });
}
}
といっても別途サービスクラスを作成する必要はなく、使用したいコンポーネントのproviders
配列に対してRxState<State>
と指定するだけです。初期値等はconstructor()
で設定します。
this.state.connect(
'list',
combineLatest([
this.globalState.select('todos'),
this.onlyViewNotDone$,
]).pipe(
map(([todos, onlyViewNotDone]) => {
if (onlyViewNotDone) {
return todos.filter((o) => !o.done);
} else {
return todos;
}
})
)
);
特徴的なのはこの部分です。this.state.connect()
は、作成したコンポーネントストアの特定プロパティに対し、Observableを「接続する」ことができます。と説明してもわかりづらいですが、つまるところ第2引数に指定したObservable
から流れてくる値を自動的に購読した上でストアにセットしてくれるものです。Observable
につきもののSubscription
も、RxStateなら明示的に破棄せずともきちんと自動破棄してくれますので、HotなObservable
を取り扱うときにも安心です。
また、Observableのパフォーマンスチューニングでよく使われるdistinctUntilChanged()
やshareReplay()
といったパイプも自動的に設定され、パフォーマンスの高いコンポーネントストアが作成できます。
一方で、ストアとして使うための便利関数の実装は最低限しかありませんので、更新や削除等の処理はBehaviorSubject
での管理と同様にすべて自分で書かなければなりません。これはグローバルストアとしての使用法を(ほぼ)想定していないからです。
import { InjectionToken } from '@angular/core';
import { Todo } from 'src/app/types/todo';
import { RxState } from '@rx-angular/state';
export interface RxAngularGlobalState {
todos: Todo[];
}
export const GLOBAL_RX_STATE = new InjectionToken<
RxState<RxAngularGlobalState>
>('GLOBAL_RX_STATE');
@NgModule({
declarations: [AppComponent],
imports: [
...
],
providers: [
{
provide: GLOBAL_RX_STATE,
useFactory: () => new RxState<RxAngularGlobalState>(),
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
import { Component, Inject } from '@angular/core';
import { RxState } from '@rx-angular/state';
import {
GLOBAL_RX_STATE,
RxAngularGlobalState,
} from './rx-angular-state/store/rx-angular-state.state';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'angular-state-example';
constructor(
@Inject(GLOBAL_RX_STATE) private state: RxState<RxAngularGlobalState>
) {
this.state.set({ todos: [] });
}
}
一応上記のようにしてグローバルストアを作成することはできますが、あんまりイケてない感じなので個人的にはお勧めしません。
公式でもグローバルストアの作成はあくまでもおまけ機能的な扱いをされていますので、グローバルストアについては別途NgRxやAkitaといったライブラリを導入したほうがよいでしょう。もともとそのような使われ方をする想定で作られたライブラリです。
まとめ
- ストアとして高機能なのは?
NgRx = Akita >>>> @rx-angular/state >> BehaviorSubject
- シンプルなのは?
BehaviorSubject >> Akita > @rx-angular/state >>>>>>>>>> NgRx
- 軽いのは?
BehaviorSubject >>>>> @rx-angular/state >>> NgRx(コアライブラリのみ) >> Akita
個人的には、小~中規模な開発に関してはAkita
で十分事足りるのではないかと感じました。NgRx
のFluxベースの頑強な設計は、多人数チームで大規模開発を行う場合にうまみが出てくると思いますが、小中規模の開発ではFluxベースのストアは逆に鬱陶しいだけです。
その点、Akita
は非常にシンプルでありながら高機能で、Angularの文化をよく解したうえで開発されているなーと感じましたし、初心者がBehaviorSubject
ストアからのステップアップを行う上でもかなり分かりやすいです。ただ、参考資料は圧倒的に少ないので、公式サイトをにらめっこしながら読み解いていく必要があります。
@rx-angular/state
はストアそのものを作るというより、「コンポーネントとグローバルストアの橋渡し役」として非常に優秀なライブラリです。使い方にクセは多少ありますが、パフォーマンスが重要なアプリには今後必須になってくるかもしれません。
総合的に判断して、私はAkita + @rx-angular/state
が好きですが、読者の皆さんにおかれましては、各ライブラリのメリットデメリットやチームのスキルレベル、プロダクトに求められる仕様等を勘案して、最適な組み合わせを見つけてもらえたらと思います。