NgRxとは、Angular用の状態管理ライブラリです。簡単に言うと、アプリケーションのデータ(状態)を一箇所で管理し、どこからでもそのデータにアクセスしたり更新したりできるようにするツールです。
アプリケーションのデータ(状態)とは、例えばログインしているユーザーのプロフィール情報だったり、ユーザーがログインしているかどうか・ユーザがショッピングカートに追加した商品の状態などが該当します。NgRxを使うと、この「データの変化」をきちんと管理しやすくなり、大きなアプリケーションでもデータの流れを追いやすくなります。
NgRxはアプリケーションのデータを効率的に管理するための道具箱のようなものです。NgRxを利用することで、アプリケーションが複雑になっても、データの管理を適切に行うことでバグを減らし、開発をスムーズに進めることができるようになります。
このページではそもそも状態管理ライブラリって何?なぜ状態管理が重要なの?NgRxはどのように利用するの?という疑問をお持ちの方向けに1からわかりやすく解説します。
「状態管理」はなぜ重要か
まず初めになぜ「状態管理が重要なのか?」について説明します。結論から言えば、状態管理はアプリの一貫性・リアルタイム性を実現するため、また開発者にとってはバグの発見と修正を早くすることを可能にするという点で重要だと言えます。
具体的に簡単なソーシャルメディアアプリを例にとってその重要性を考えていきたいと思います。
1. ユーザーの投稿リストの例
アプリにログインすると、友達やフォローしている人たちの最新の投稿がフィードに表示される機能の場合。まず、ここでの「投稿リスト」が1つの状態管理です(ユーザの友達・フォローしている人などによって状態が変わるので)。
ユーザーが新たに投稿を行うと、その投稿はリストの最上部に追加され、他のユーザーが投稿をするたびに、フィードがリアルタイムで更新されます。ログインするユーザによって何を表示するのか?(=表示すべき投稿の内容)を管理する仕組みが重要になります。
2. 「いいね!」やコメントの例
投稿に対して「いいね!」をするかコメントをすると、その情報を即座に投稿の状態に反映する機能を考えてみましょう。たとえば、あなたがある投稿に「いいね!」をした場合、その「いいね!」の数が更新され、他の人がその投稿を見たときにもその更新が反映されているべきです。この「いいね!」やコメントも、1つの状態管理です。
3. 通知システム
誰かがあなたの投稿に「いいね!」をしたり、コメントをしたりすると、通知が届きます。この通知もまた、アプリの状態の一部であり、リアルタイムで更新される必要があります。
また、考えてみれば当たり前のことですが、同じアプリを異なるデバイスで開いたとき(たとえばスマホとタブレット)、または同じデバイスでも異なる画面では(たとえばフィード画面とプロフィール画面)、ユーザーに対して一貫した情報(投稿、コメント、いいねの数)を表示する必要があります。もし、状態管理がしっかりしていなければ、これらの情報を一元的に更新し、どこからアクセスしても同じ情報を見ることはできなくなってしまいます。
このように、状態管理は特に複雑なWebアプリケーションにおいて非常に重要な役割を担います。もし、状態管理が適切になされていなければ、ユーザのプロフィールが同時にログインしている他のユーザから参照できてしまったり、自分の投稿が他の人から見られなくなってしまったり・・・というような弊害が出てきます。
この状態管理をしやすくするためのAngularのツールの1つがNgRxです。
NgRxを使った状態管理の仕組み
NgRxを使った状態管理の仕組みを解説します。結論から言うと、NgRxでは以下4つの概念で1つの状態を管理しています。
ポイント NgRxの4つの主要な部品
- ストア(Store)
- アプリのデータ(状態)が全部入っている大きな箱のようなもの。アプリのどこからでもこの箱の中身を見ることができ、必要なデータを取り出したり、新しいデータを追加したりすることができます。
- アクション(Actions)
- ユーザーが何か行動を起こしたとき(例えば、新しい投稿をする、コメントをする、いいね!をするなど)、その行動をアプリに伝えるメッセージです。このメッセージは「何をしたいのか」という情報を伝えます。
- リデューサー(Reducers)
- アクションからメッセージが来たときに「ストアのデータをどう変えるか」を決めるルールブックです。例えば、「新しい投稿アクション」が来たら、「投稿リストに新しい投稿を追加する」といった具体的な指示が含まれます。
- エフェクト(Effects)
実際のアプリを例にその流れを大まかに説明すると、ユーザーが新しい投稿を作成するとき「新しい投稿アクション」が発行される→このアクションはリデューサーによって処理され、ストアの「投稿リスト」データが新しい投稿で更新される→エフェクトがこのアクションを検知し、新しい投稿をサーバーに保存するための処理を行う、というような流れ。
つまり、このような仕組みでNgRxはアプリのデータ(状態)の変更を一元管理し、複雑なアプリケーションでもデータの流れを追跡しやすく、かつ効率的に非同期処理を管理します。
上記の図はNgRx理解の最重要ポイントです。
NgRxの実装
ここからは、上記の基本知識をさらに深めるため、実際にAngularでNgRxライブラリを用いたコードを記述していきたいと思います。
なんといっても、実装ができてなんぼの世界なので、1個1個丁寧に理解しておきましょう。
Store(ストア)のセットアップ
まず、アプリケーションのルートモジュールにNgRxストアをセットアップします。ストアは、アプリケーションの状態を保持するための容器です。これを設定することで、アプリケーション全体で状態を一貫性を持って管理できるようになります。
ステップ1: NgRxモジュールのインストール
まず、プロジェクトにNgRxをインストールする必要があります。コマンドラインまたはターミナルで以下のコマンドを実行して、NgRxをプロジェクトに追加します。
yarn add @ngrx/store
「NgRxをインストールする」というのは、実際には「NgRxのライブラリを自分のプロジェクトに追加する」という意味です。ライブラリとは、あらかじめ書かれたコードのセットで、特定の機能を提供するものです。
ステップ2: アプリケーションモジュールの設定
NgRxのストアをセットアップするためには、アプリケーションのルートモジュール(AppModule
)にStoreModule
をインポートし、そのforRoot
メソッドを使ってストアを構成します。ここでは、まだ具体的なリデューサーを定義していないので、単にストアをセットアップする基本的なステップを踏みます。
import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; @NgModule({ imports: [ // ...他のモジュールのインポート StoreModule.forRoot({}, {}) // ここで空のリデューサーマップと構成オプションを指定 ], // ...他のNgModuleメタデータ }) export class AppModule { }
↑のコードでは、forRoot
メソッドに空のオブジェクトを2つ渡しています。1つ目の空のオブジェクトは、アプリケーションの状態を管理するリデューサーの関数を指定するのですが、現時点ではリデューサーがまだないため空にしています。
Actions(アクション)の定義
アクションは、アプリケーションで何かが起きたことを伝えるメッセージのようなものです。例えば、「ログインボタンがクリックされた」「商品がカートに追加された」といったユーザーの操作や、アプリケーションの状態変化を示します。
早速Actions(アクション)の定義をしていきましょう。
ステップ1: アクションの目的を決める
まず、どんな操作やイベントに対してアクションが必要かを考えます。例えば、ユーザー認証なら「ログイン実行」「ログイン成功」「ログイン失敗」が考えられます。
ステップ2: アクションファイルを作成する
src/app/actions
ディレクトリに、機能ごとに分かれたアクションファイル(例:auth.actions.ts
)を作成します。一応ディレクトリはどこに作成しても良いのですが、1つのディレクトリにアクションファイルを集めておくのがよく見かける管理方法の1つです。
ステップ3: createAction
を使ってアクションを定義する
@ngrx/store
では、createAction
関数を利用してアクションを定義します。まず、この関数を使えるようにするために、@ngrx/store
からcreateAction
をインポートします。
import { createAction } from '@ngrx/store';
次に、createAction
関数を使ってアクションを定義。この関数には少なくとも1つ、アクションの種類を示す文字列:アクションタイプを渡します。アクションタイプ(Action Type)は、NgRxなどの状態管理ライブラリにおいて、アクションを一意に識別するための文字列です。
export const loginStart = createAction('[Auth] Login Start');
ステップ4: 必要に応じてペイロードを定義
アクションに追加のデータ(ペイロード)を含めたい場合は、props
関数を使ってその形状を定義します。
import { createAction, props } from '@ngrx/store'; export const loginStart = createAction( '[Auth] Login Start', props<{ username: string; password: string }>() );
ステップ5: アクションをエクスポートする
定義したアクションは、他のファイルから使えるようにエクスポートします。これで、アクションをディスパッチする準備が整います。
まとめると以下のような感じですね。
import { createAction, props } from '@ngrx/store'; // ユーザーログインのアクション定義 export const loginStart = createAction( '[Auth] Login Start', props<{ username: string; password: string }>() ); // ログイン成功時のアクション定義 export const loginSuccess = createAction( '[Auth] Login Success', props<{ userId: string; token: string }>() ); // ログイン失敗時のアクション定義 export const loginFailure = createAction( '[Auth] Login Failure', props<{ error: string }>() );
ちなみに、上記のコードを簡単に説明すると以下の通りになります。
loginStart
- ユーザーがログインを試みる際にディスパッチされ、ユーザー名とパスワードをペイロードとして含みます。
loginSuccess
- ログインプロセスが成功した際にディスパッチされ、ユーザーIDと認証トークンをペイロードとして含みます。
loginFailure
- ログインが失敗した際にディスパッチされ、エラーメッセージをペイロードとして含みます。
このようにアクションを定義することで、アプリケーションの状態管理ロジックがどのようなイベントに反応するべきか、そしてそれに応じてどのような状態変化を適用するかを明確にできます。
Reducers(リデューサー)の作成
リデューサーは、アクションが発生したときにアプリケーションの状態をどのように変更するかを定義する関数です。ざっくり言えば「今の状態とアクションを受け取って、新しい状態を返す」という役割を担います。
ステップ1: 状態の形状を定義する
まず、管理したいデータ(状態)の形状を決めます。例えば、認証状態なら、ユーザー情報やログイン状態を含むオブジェクトになるでしょう。
interface AuthState { user: User | null; isAuthenticated: boolean; }
ステップ2: 初期状態を定義する
次に、アプリケーションが最初に起動したときの状態(初期状態)を定義します。
const initialState: AuthState = { user: null, isAuthenticated: false };
ステップ3: リデューサー関数を作成する
リデューサー関数を作成し、アクションタイプに応じて状態を更新するロジックを書きます。
function authReducer(state: AuthState = initialState, action: Action): AuthState { switch (action.type) { case '[Auth] Login Success': return { ...state, user: action.payload.user, isAuthenticated: true }; // 他のアクションタイプに対する処理 default: return state; } }
この例では、[Auth] Login Success
アクションが発生した場合に、ユーザー情報を更新し、isAuthenticated
をtrue
に設定しています。
先ほど定義したアクションファイルと見比べてみましょう。以下で定義したアクションと、今回定義したリデューサーがどのように対応しているか?にポイントを置きながら。
import { createAction, props } from '@ngrx/store'; // ユーザーログインのアクション定義 export const loginStart = createAction( '[Auth] Login Start', props<{ username: string; password: string }>() ); // ログイン成功時のアクション定義 export const loginSuccess = createAction( '[Auth] Login Success', props<{ userId: string; token: string }>() ); // ログイン失敗時のアクション定義 export const loginFailure = createAction( '[Auth] Login Failure', props<{ error: string }>() );
ステップ4: ストアへのリデューサーの登録
リデューサーが準備できたら、アプリケーションの状態管理のためにNgRxストアに登録します。これは通常、アプリケーションのルートモジュール(例:AppModule
)で行います。
import { StoreModule } from '@ngrx/store'; import { authReducer } from './auth.reducer'; @NgModule({ imports: [ StoreModule.forRoot({ auth: authReducer }), // 他のモジュールのインポート ], // その他のNgModuleメタデータ }) export class AppModule {}
このステップを通じて、アクションがディスパッチされたときにリデューサーが呼び出され、アプリケーションの状態が適切に更新されるようになります。コンポーネントやサービスからは、アクションをディスパッチすることで、リデューサーによる状態の変更を間接的に行うことができます。
リデューサーの作成方法/細かい構文ルールは以下でも解説しております。
Effects(エフェクト)の実装
エフェクトは、アプリケーションの外部とのやりとり(例えば、データベースへの問い合わせや外部APIの呼び出し)を管理するための仕組みです。エフェクトを使うことで、ユーザーの操作やシステムのイベントが発生したときに、それに応じて必要な副作用(外部との通信など)を実行し、その結果を元にアプリケーションの状態を更新します。
アクションが「何かをしてほしい」という要求であると考えると、エフェクトはその要求に対して「では、これをしましょう」と応答する役割を持ちます。その結果を元にまた別のアクションを起こして、アプリケーションに知らせるのがエフェクトです。
ステップ 4.1: エフェクト用のモジュールをインストール
まず、NgRxエフェクトを使用するために必要なモジュールをインストールします。yarnを使用して、プロジェクトに@ngrx/effects
を追加します。
yarn add @ngrx/effects
ステップ 4.2: エフェクトクラスを作成
エフェクトクラスを作成します。
// auth.effects.ts import { Injectable } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { AuthService } from '../services/auth.service'; import * as AuthActions from './auth.actions'; @Injectable() export class AuthEffects { constructor( private actions$: Actions, private authService: AuthService ) {} }
ステップ3: エフェクトを定義
ログインアクションをリッスン(=監視)し、成功または失敗の結果をもとに新しいアクションをディスパッチするエフェクトを定義します。
login$ = createEffect(() => this.actions$.pipe( ofType(AuthActions.loginStart), mergeMap(action => this.authService.login(action.username, action.password).pipe( map(user => AuthActions.loginSuccess({ user })), catchError(error => of(AuthActions.loginFailure({ error }))) ) ) ) );
ステップ4: AppModuleにエフェクトを登録
最後に、作成したエフェクトをアプリケーションに登録します。これには、AppModule
にEffectsModule.forRoot([])
を追加し、その配列内にエフェクトクラスを指定します。
import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from './auth.effects'; @NgModule({ imports: [ // 他のモジュール EffectsModule.forRoot([AuthEffects]), ], }) export class AppModule {}
以上でエフェクトの実装は完了です。
セレクタの使用/コンポーネントとの統合
ここからは、定義したアクションやストアなどを実際にアプリケーションの中で「用いるステップ」に関する解説となります。これらのステップは、NgRxを用いた状態管理の流れの中で、状態の読み取りや更新を行うための実践的な部分です。
NgRxとコンポーネントを統合する方法を解説して本ページの解説を終えます。
Selector(セレクタ)の使用
セレクタは、アプリケーションの状態(State)の中から、特定の情報を取り出すためのツールです。ある意味で「状態のクエリ」のようなものと考えることができます。例えば、アプリ全体の状態が本棚のようなものだとしたら、セレクタはその棚から読みたい本を選び出す役割を持っています。
セレクタの作り方
まず、アプリの状態(データ)の中から何を取り出したいのかを決めます。例えば、「ユーザーの名前」や「商品リスト」などです。次に、そのデータを取り出すためのセレクタ関数を作ります。NgRxにはcreateSelector
という便利な関数があり、これを使ってセレクタを作ることができます。
import { createSelector } from '@ngrx/store'; // 全体の状態からユーザー部分の状態を取り出す const selectUserState = (state) => state.user; // ユーザー部分の状態から名前だけを取り出す export const selectUserName = createSelector( selectUserState, (user) => user.name );
セレクタの使い方
セレクタができたら、コンポーネントでそれを使ってデータを取り出します。コンポーネントでは、select
メソッドを通じてストアから特定の状態を選択するためにセレクタを使用します。これにより、必要なデータのみを効率的に取得できます。
count$ = this.store.select(selectFeatureCount);
次に、コンポーネントからユーザーの操作やイベントに応じてアクションをストアにディスパッチします。これにより、アプリケーションの状態を更新する流れが始まります。
onLogin(username: string, password: string) { this.store.dispatch(login({ username, password })); }
最後に、コンポーネントでストアを利用するために、まずコンストラクタ内でストアを注入します。以下は、ストアと統合されたコンポーネントの例です。
@Component({ selector: 'app-my-component', template: ` <div>Count: {{ count$ | async }}</div> <button (click)="onLogin('username', 'password')">Login</button> `, }) export class MyComponent { count$ = this.store.select(selectFeatureCount); constructor(private store: Store<AppState>) {} onLogin(username: string, password: string) { this.store.dispatch(login({ username, password })); } }