Angularのngrxを使って状態管理を行う（理論編）

> この記事は[Angular+Firebaseでチャットアプリを作る](http://qiita.com/Yamamoto0525/items/a76ea4b3924eeb82b0f9)のエントリーです。
> 前記事：[Angular+Firebase ユーザー設計とデータの保護](https://qiita.com/Yamamoto0525/items/e94fc6da25b2d2524332)
> 次記事：[Angularのngrxを使って状態管理を行う（実装編：初期設定～エフェクト設定）](https://qiita.com/Yamamoto0525/items/6c31cd6b45d23e54637b)

# この記事で行うこと

[前回の記事](https://qiita.com/Yamamoto0525/items/e94fc6da25b2d2524332)ではユーザー設計とデータの保護を扱いました。
ここからAngularの`@ngrx`というライブラリを使って、状態管理の理論、実装を３回にわけて行っていきます。

本記事ではまず、状態管理の理論について学習します。

- [なぜngrxを使う必要があるのか](#なぜngrxを使う必要があるのか)
- [ngrxの流れを見る](#ngrxの流れを見る)
- [Effectで非同期のデータを扱う](#effectで非同期のデータを扱う)


### `@ngrx`とは

`@ngrx`はReduxというライブラリに影響を受けて作成されたAngularの公式ライブラリです。Actions、Dispatcher、Reducer、State、ViewというReduxのデータフロー構成を、RxJSとクラスを使ってAngular流に再現しています。

**Reduxのデータフロー**

1. View（Component）でユーザーがクリックするなどの動作でActionを発行
2. DispatcherがActionをStoreに渡す
3. Actionの結果をReducerで元のStateと合成する
4. stateをViewに返して、Viewを更新する

![Redux図 (1).png](https://qiita-image-store.s3.amazonaws.com/0/112566/4529abf3-63ce-0596-5c47-2ea330cfa526.png)


また、`@ngrx`は[ngrx/platform](https://github.com/ngrx/platform)で開発が進められており、2020/6月現在では下記ライブラリが利用できます。

| ライブラリ名|概要|
|:-----------------|:------------------|
|`@ngrx/store`|Reduxに影響を受けた、RxJSベースのAngularアプリケーション用状態管理ライブラリ|
|`@ngrx/store-devtools`|リアルタイムでストアの状態を把握するデバッグ用ライブラリ|
|`@ngrx/effects`|発行されたActionを元にWebAPIにアクセスするといった「副作用」を処理するライブラリ|
|`@ngrx/router-store`|Angular Routerと@ngrx/storeを連携させるライブラリ|
|`@ngrx/entity`|エンティティ（複数配列データ）の管理ができるライブラリ|
|`@ngrx/data`|エンティティデータ管理を簡略化する拡張機能|
|`@ngrx/component`|フルリアクティブかつZoneを利用しないアプリケーション用拡張機能(2020年6月時点で開発中)|
|`@ngrx/component-store`|ローカルにあるコンポーネントストアを管理するためのスタンドアロンライブラリ|
|`@ngrx/schematics`|ngrxのコードジェネレーターライブラリ|

#### 参考
[ngrx紹介](https://qiita.com/kouMatsumoto/items/c8297466c1824953632f)

# 本題

## なぜngrxを使う必要があるのか

[以前の記事](https://qiita.com/Yamamoto0525/items/efc0e5617c7427fea86a)で、Angularのデータ管理にはRxJSを使用するということを書きました。あるコンポーネントで利用したデータをほかのコンポーネントでも利用したい場合、RxJSのストリーム（Observable）にデータを流すことで実現できます。

![Redux図 (2).png](https://qiita-image-store.s3.amazonaws.com/0/112566/ae904094-413a-1f85-73f5-182e73cc9b1a.png)

規模の小さいアプリケーションであれば、上記のような構成でもそれほど問題になりません。しかし、アプリケーションの規模が大きくなってObservableの数が増え始めると、それぞれのObservableの配線が複雑になって、Stateがどのような状況になっているかが不明瞭になっていきます。

![Redux図 (3).png](https://qiita-image-store.s3.amazonaws.com/0/112566/6a0126cb-11da-5b57-b080-1b8a7f860604.png)

状態管理が複雑になってしまうと、何か不具合があったときにどのObservableによるものなのかが判断しづらく、解消に時間がかかってしまったり、不要なObservableがメモリを圧迫してパフォーマンスに影響を与えてしまうようなことが考えられます。

この事態を避けるために有効な手段が`@ngrx`ライブラリの導入です。状態管理に一定の規則性を与え、1つの大きなオブジェクト（ステートツリー）で管理することで上記の懸念を解消できます。

![Redux図 (5).png](https://qiita-image-store.s3.amazonaws.com/0/112566/f8609fa3-fa94-461c-4302-fcc4c3397f60.png)


また、`@ngrx`の導入には、複雑性の解消以外にも次のようなことが期待できます。

- 開発者間での共通な設計思想
- コンポーネントテストの容易性

これらの有効性については[こちらの記事](https://qiita.com/kouMatsumoto/items/c8297466c1824953632f#ngrx%E3%82%92%E4%BD%BF%E3%81%86%E7%90%86%E7%94%B1)を参照してください。

## ngrxの流れを見る

具体的な実装方法については次回以降の記事で扱うので、ここでは各構成の役割を流れに沿って確認していきます。

### ①View → Dispatcher

![Redux図 (13).png](https://qiita-image-store.s3.amazonaws.com/0/112566/7d8f380b-9d70-25be-4fe2-b1b7ee2dac9b.png)


まずはView（コンポーネント）でActionを作成し、Dispatcherに渡します。

```ts:session.action.ts
export enum SessionActionTypes {
  LoadSessions = '[Session] Load',
  LoginSessions = '[Session] Login',
  LogoutSessions = '[Session] Logout'
}

export class LoadSessions implements Action {
  readonly type = SessionActionTypes.LoadSessions;
}

export class LoginSessions implements Action {
  readonly type = SessionActionTypes.LoginSessions;
}

export class LogoutSessions implements Action {
  readonly type = SessionActionTypes.LogoutSessions;
}

export type SessionActions = 
 LoadSessions
 | LoginSessions
 | LogoutSessions;
```

```ts:app.component.ts
export class AppComponent {

  constructor(private store: Store<fromCore.State>) {
    this.store.dispatch(new LoadSessions());
   }

  login() {
    this.store.dispatch(new LoginSessions());
  }

  logout() {
    this.store.dispatch(new LogoutSessions());
  }

}
```

ActionはView上で生じる操作（クリック、スクロール）や、データの読み込みといった、データ（State）の変化を生じさせるトリガーを定義したものです。

ここではセッションデータの読み込み、ログイン、ログアウトといった操作を定義し、`dispatch()`の引数にすることでReducerにActionを渡しています。

### ②Dispatcher → Reducer → State

![Redux図 (14).png](https://qiita-image-store.s3.amazonaws.com/0/112566/c5ce794b-c1eb-767b-b5e2-a91d7994e561.png)


続いてDispatcherで受けたActionをStoreにあるReducerに渡します。

```ts:session.reducer.ts
/**
 * State
 */

export interface State {
  session: { login: boolean };
}

export const initialState: State = {
  session: { login: false };
};

/**
 * Reducer
 */

export function reducer(
  state = initialState,
  action: SessionActions
): State {
  switch (action.type) {
    case SessionActionTypes.LoadSessions: {
      return { ...state };
    }

    case SessionActionTypes.LoginSessions: {
      return { session: { login: true } };
    }

    case SessionActionTypes.LogoutSessions: {
      return { session: { login: false } };
    }

    default:
      return state;
  }
}

export const getSession = (state: State) => state.session;

```

Reducerでは最初にStateの型と初期値（`initialState`）を定義します。
初めて`dispatch()`された時は、この`initialState`がstateとして流れてきます。

`reducer`クラスには現在のstateとDispatcherで指定されたactionが引数として渡され、actionのタイプによって異なる処理が加えられます。

流れてきたActionのタイプが`LoginSessions`だった場合、session stateは`{ login: true }`に上書きされ、`LogoutSessions`だった場合は`{ login: false }`に上書きされます。

`getSession()`は取得したいstateを指定するためのメソッドです。Viewからstateを指定するときは、このメソッドを使うことになります。

### ③State → View

![Redux図 (15).png](https://qiita-image-store.s3.amazonaws.com/0/112566/68e488a0-7871-3cf0-9616-1341c1732ac5.png)


最後は②で上書きしたStateをViewに渡して反映させます。

```ts:header.component.ts
export class HeaderComponent implements OnInit {

  public session$: Observable<{login: boolean}>;

  constructor(private store: Store<fromCore.State>) {
    this.session$ = this.store.select(fromCore.getSession);
  }

  ngOnInit() {
  }
}
```

`this.store.select()`で取得したいstateを指定し、`session$`に格納しています。
これにより他のコンポーネントで更新されたStateを、リアルタイムで反映することができるようになります。

`@ngrx`の基本的なデータの流れは以上です。
次は非同期通信で外部からデータを取得した場合について考えます。

## Effectで非同期のデータを扱う

非同期通信でデータを取得する場合、どのような流れになるのかを図示してみます。

![Redux図 (7).png](https://qiita-image-store.s3.amazonaws.com/0/112566/ba2c002b-4e68-3765-9ac4-686f7c51ab63.png)

最初の図に`@Effect`という概念が追加されました。非同期通信時は、この`@Effect`を使ってデータの取得とStateの更新を行います。

`@Effect`は特定のActionがDispatcherに渡された瞬間から開始され、非同期通信の結果が取得でき次第、その結果を再びDispatcherに渡します。もちろん、その結果には通信の成功と失敗の両方がありますので、それに合わせて新しいActionを用意する必要があります。

> このときの処理の流れは、[こちら](http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/27)を参照するとわかりやすいです。

`@Effect`上記のをコードに反映すると次のようになります。

```ts:session.action.ts
export enum SessionActionTypes {
  LoadSessions = '[Session] Load',
  LoadSessionsSuccess = '[Session] Load Success',
  LoadSessionsFail = '[Session] Load Fail',
  LoginSessions = '[Session] Login',
  LogoutSessions = '[Session] Logout'
}

export class LoadSessions implements Action {
  readonly type = SessionActionTypes.LoadSessions;
}

export class LoadSessionsSuccess implements Action {
  readonly type = SessionActionTypes.LoadSessionsSuccess ;

  constructor(public payload: { session: { login: boolean } }) {}
}

export class LoadSessionsFail implements Action {
  readonly type = SessionActionTypes.LoadSessionsFail ;

  constructor(public payload?: { error: any }) {}
}

export class LoginSessions implements Action {
  readonly type = SessionActionTypes.LoginSessions;
}

export class LogoutSessions implements Action {
  readonly type = SessionActionTypes.LogoutSessions;
}

export type SessionActions = 
 LoadSessions
 | LoadSessionsSuccess
 | LoadSessionsFail
 | LoginSessions
 | LogoutSessions;
```
```ts:session.reducer.ts
/**
 * Reducer
 */

export function reducer(
  state = initialState,
  action: SessionActions
): State {
  switch (action.type) {
    case SessionActionTypes.LoadSessions: {
      return { ...state };
    }

    case SessionActionTypes.LoadSessionsSuccess: {
      return { session: action.payload.session };
    }

    case SessionActionTypes.LoadSessionsFail: {
      if (action.payload) {
        console.log(action.payload.error);
      }
      return { ...state };
    }

    case SessionActionTypes.LoginSessions: {
      return { session: { login: true } };
    }

    case SessionActionTypes.LogoutSessions: {
      return { session: { login: false } };
    }

    default:
      return state;
  }
}

```
```ts:session.effect.ts
export class ChatEffects {

  constructor(private actions$: Actions,
              private afAuth: AngularFireAuth) {
  }

  @Effect()
  loadSession$: Observable<Action> =
    this.actions$.pipe(
      ofType<LoadSessions>(SessionActionTypes.LoadSessions),
      map(action => action),
      switchMap(() => {
        return this.afAuth.authState
          .pipe(
            take(1),
            map( result => {
              const login_state = { login: (!!result) };
              return new LoadSessionsSuccess({ session: login_state })
            }),
            catchError( err => new LoadSessionsFail({ error: err }))
          );
      })
    );
}
```

ActionとReducerに`LoadSessionsSuccess`と`LoadSessionsFail`を加え、それぞれの初期値と返り値を定義しています。
Effectを使う場合は、まず`@Effect()`デコレータを宣言します。`ofType()`で対象となるActionを指定し、そのActionが`dispatch()`されると同時に処理が始まります。

今回は`this.afAuth.authState()`でログイン状況を問合せ、その結果を`LoadSessionsSuccess()`でDispatcherに渡しています。そのデータはその後Reducerに渡され、現在のstateを上書きしてまたView（コンポーネント）へと渡されていきます。

## まとめ

`@ngrx`はReduxとRxJSの両方の知識が必要となり、習得にはある程度時間がかかるかと思われます。
しかし、SPAを始めとした近年のWEBアプリケーションでは、クライアントサイトでのデータ処理が増えているため、体系化した状態管理のしくみが求められています。今後、新しいWEBアプリケーションを構築する、もしくは既存アプリケーションの状態管理を体系化するといったことを予定している場合は、`@ngrx`の導入を検討してみてください。

ここまで、`@ngrx`の概念と基本的な構成を扱いました。次回の記事では、具体的な実装方法について取り上げていきます。