athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

NgRxを用いたAngularアプリの状態管理をする話

はじめに

  こんにちは、Cシステム開発グループのチンです。前回(入社3年目に体験したハイブリッドアプリ開発 - athome-developer’s blog) で紹介した通り、当社ではAngularでハイブリッドアプリケーションを開発してます。今回はフロントエンドで使用している状態管理ライブラリ(NgRx)について、簡単に紹介したいと思います。

前提知識

  • Angular
    - Webアプリフレームワークの一種
  • TypeScript
    - Angularの推奨言語
    - JavaScriptの上位互換言語なので、JSを書けば大体分かる
  • RxJS
    - リアクティブプログラミング用のライブラリの一つ
  • NgRx
    - Angularで使用する状態管理ライブラリの一つ
    - Redux(Reactの状態管理ライブラリ)にインスピレーションを得た
    - Reduxの三原則に則った上、さらに役割を細分化

なぜ、NgRxで状態管理するのか

なぜ、アーキテクチャを決めるのか:

フロントエンドのプログラミングは、自由度がかなり高いです。
個人開発としては良いことですが、多人数で開発する場合、ある程度のルールに制約されないと
お互いの考えを理解するだけで時間がかかり、後からのテストもメンテナンスも難しくなります。
早期段階アーキテクチャを定めると、開発がスムーズに進められます。

なぜ、状態管理が必要なのか:

今回の開発アプリがSPAであることや、UI面での要求により、状態管理が必要となりました。

SPA

今回開発するアプリは、スマートフォンアプリでもあり、Webアプリでもあり、本質的にはシングルページアプリ(SPA)の一種です。その名前通り、ただ一つのページで構成されたアプリです。
従来のウェブサイトと比較し、ページを切り替える際に下記の違いがあります。

  • 従来のウェブサイト:バックエンドで組んだページの情報を、フロントエンドに送って表示します。

  • SPA:新しいページを開かずに必要なデータだけをリクエストし、JavaScriptで現在の画面を上書きします。

SPAは、初期読み込みが若干遅くなりますが、ページ表示用のデータを重複取得せずに遷移できるので、通信量が減り、サーバーへの負荷も軽くなります。
一方で、アプリ全体として、「必要なデータ(状態)」を「どのタイミングで取得するか、どうやって更新するか、いつまで保持するか」を考慮しなければならなくなりました。

UI

現在、「状態変化に動的に反応してくれるUI」が主流となっており、今回の開発では「リアクティブプログラミング」の手法を用いて、実現しています。

  • 従来の開発手法:状態を変えるたびに画面上の要素も更新する。
  • リアクティブプログラミング:
    • 特定な場所(store)に状態を管理する。
    • 状態が変わるたび、画面上の要素も変わるように連動させる。

従来の方法で、「状態変化に動的に反応してくれるUI」を実現しようとすると、仕様が追加・変更されるたび、その分の処理変更が必要な上、既存の処理との衝突やデータの流れの偏差に対する配慮も必要となり、工数が増えます。
リアクティブプログラミングの手法では、常に最新の状態を検知できるので、あらゆる仕様変更に耐えられるようになります。

なぜ、NgRxにするのか:

NgRxは、Fluxというwebアーキテクチャを採用しており、下記のようの特徴があります。

  • イベント駆動
  • データの流れは単一方向
  • 部品の責務は明確に切り分けられる

NgRxを採用したメリットとして

  • イベント単位で実装したので、何か既にできているのか分かりやすい
  • 仕様が変わっても、イベントで探せばすぐ修正箇所が分かる
  • どのファイルが何をするかが明確だから、迷いが少ない
  • データの流れる方向が確定なので、デバッグが簡単
  • とりあえず「日々片付けている部屋」感があって心地が良い

などがあると、個人的に感じています。


NgRxを導入したプロジェクト構造

NgRxを導入したプロジェクト構造と各パーツの責務は、だいたい下記のようになります。

f:id:chen_at:20210713162915p:plain
図 1. https://ngrx.io/guide/store

各パーツの責務:
 Store: 状態を格納する
 State: 「状態」そのもの
 Reducer: 状態に書き込む
 Selector: 状態を購読する
 Component: UIが配置される所(AngularでのView)
 Action: あらゆるのイベント
 Effects: サイドエフェクトを処理する

NgRxによる状態管理のサンプルアプリ

抽象的な話が続きましたが、ここからは、サンプルアプリを例に 「状態管理」と「処理の流れ」を具体的に説明しようと思います。
ソース全体はサンプルアプリ(StackBlitz↗︎)をご覧ください。

f:id:chen_at:20210715101357p:plain
図2. サンプルアプリ

表1. サンプルアプリの機能と管理する状態

イベント 処理 状態
「読者になる」をクリック ①読者になる
②フォロワー数が増える
  • isFollower:読者であるかどうか
  • count:読者数
「読者をやめる」をクリック ①読者をやめる
②フォロワー数が減る
状態管理

まず、状態管理について説明します。
今回は下記のように、「isFollower: 読者であるかどうか、count: 読者数」という2つの状態を管理しています。

follow.reducer.ts
/** 管理したい状態を定義 */
export class FollowState {
  isFollower: boolean;
  count: number;
}

/** 初期状態 */
export const initialState: FollowState = {
  isFollower: false,
  count: 36
};
処理

次に、「読者になる」をクリックした場合の「処理」を説明します。
なお、①~④のデータフローは「NgRxを導入したプロジェクト構造」の図1の左半分が該当します。


Component -> Action:
ボタンをクリックすることで、「読者になる」のActionが発信(Dispatch)されます。

app.component.ts
export class AppComponent implements OnInit {
  /** 読者になってるか */
  isFollower$: Observable<boolean> = this.store.pipe(select(selectIsFollower));
  /** 読者数 */
  count$: Observable<number> = this.store.pipe(select(selectFollowCount));

  constructor(private store: Store) {}
  onClickSubscribe() {
    this.isFollower$.pipe(first()).subscribe(isFollower => {
      if (isFollower) {
        // 「読者になる」のActionを発信
        this.store.dispatch(actions.unfollow());
      } else {
        // 「読者をやめる」のActionを発信
        this.store.dispatch(actions.follow());
      }
    });
  }
}


Action -> Reducer -> State:
ReducerがActionに動かし、Stateに書き込みをします。

follow.reducer.ts
const reducer = createReducer(
  /** 初期状態 */
  initialState,
  /** 「どのActionがdispatchされたら何をする」を定義する */
  on(actions.follow, state => {
    const newState = {
      count: state.count + 1,
      isFollower: true
    };
    return newState;
  }),
  on(actions.unfollow, state => {
    const newState = {
      count: state.count - 1,
      isFollower: false
    };
    return newState;
  }),
  // 下略
);


State -> Selector:
Stateに変更が起こった際に、最新な情報をSelectorに流します。

follow.selector.ts
export interface AppState {
  followState: FollowState;
}  
export const selectFeature = createFeatureSelector<AppState, FollowState>(
  featureKey
);  

/** (アプリ全体の状態から)followの状態からcount数を取得*/
export const selectFollowCount = createSelector(
  selectFeature,
  (state: FollowState) => state.count
);  
/** (アプリ全体の状態から)followの状態からisFollowerを取得*/
export const selectIsFollower = createSelector(
  selectFeature,
  (state: FollowState) =>  state.isFollower
);  


Selector -> Component:
ComponentはSelectorに経由し、最新なデータを取得・表示する。

app.component.html
<p>アットホーム開発者ブログ</p>
<p>isFollower: {{isFollower$ | async}}</p>
<p>count: {{count$ | async}}</p>

<button (click)="onClickSubscribe()">
  {{(isFollower$ | async) ? "読者をやめる" : "読者になる"}}
</button>


⑤ サイドエフェクト
「NgRxを導入したプロジェクト構造」にも書いた通り、各部品は規定される責務だけを行うというルールがあります。

もうちょっと詳しく書くと:

  • Storeに直接にアクセスすることはできず、書き込みは必ずReducer、読み取りはSelectorに経由しなければならない。
  • Reducerで行う状態変更は、全て純粋関数である。
  • サーバーに送信などの処理はサイドエフェクトといい、Effectsでまとめてやってくれる。

というような規定があります。

実際業務で作った処理を例にすると、

  • お気に入り数更新処理:お気に入り登録成功時に、最新のお気に入り数も再取得してStateに上書きする。
  • アプリの起動時処理:オフラインでも確認できるように、閲覧データを更新する時、ローカルストレージにも保存し、起動時にLS -> Stateにロードする。
  • 地図情報の管理:地図の状態を管理するための情報をComponentStoreに集約して管理する。地図が持つ画面が閉じられた時に保持した状態もクリーンアップされ、アプリ全体のパフォーマンスが良くなる。
    ...

といった運用もできます。


終わりに

最後にもうちょっと自分なりの感想を話したいと思います。

私はプログラミング未経験者としてアットホームに入社し、
今は2年目です。
1年目の時に社外研修を受けて、HTML、javaScriptPHPを用いたWeb開発の基本を勉強しました。
その後の社内研修(以前のブログ)で、初めてAngularプロジェクトを作ることになりました。
研修段階では、メソッド単位のコードの優劣を判断できても、大きな実装の方向(eg.依存関係に影響与えるレベル)は先輩たちに聞かないと判断できないということがよくありました。
「それは本当にアーキテクチャの運用を理解したと言えるのか?」と、少し不安がありました。

今年はアプリ開発のプロジェクトに参加できたおかげで、実務向けなコードに触れられました。
遊び気分で作成したものと段違いな複雑さがある上、開発中に機能が追加・変更されることもあるので、プログラムの拡張性と保守性の重要さを実感しました。
また、自分の作業は、後で誰かが読んだり、直したり、使い回したり、もしくは削除されることを意識しつつ、これまで飲み込めなかった疑問を段々解消できました。
そして作業する際に、今まで直感に頼っていた部分も、きちんとした理論に基づいて実装案の優劣を付けられるようになりました。

このように自分の成長が実感できることが、エンジニアとしての楽しさの一つだと思います。
未経験者だけどエンジニアになりたい方も、「物事の仕組みを知りたい」「難しいことでもチャレンジしたい」といった心持ちがあれば、きっと大丈夫だと(個人的には)思います。

お読みいただきありがとうございました。