LoginSignup
76
58

More than 5 years have passed since last update.

Ngrxをこれから始める人への入門ガイド

Last updated at Posted at 2018-12-13

はじめに

この記事はAngular AdventCalendar2018 14日目の記事です。

今までReact、Vueを使うことが多かったんですが、半年前からプロダクトでAngular + Ngrxを扱うことになりました。
Angularを使い始めて思ったのは、ググっても他のFWと比べると記事が少ないなと思うのと、Ngrxについてはさらに少ないな、と思ったのでこれからNgrxを使おうと思っている人が増えるようにNgrx初心向けの導入記事です。

Ngrxとは

Ngrx公式ページ。Angular.ioにあわせたドキュメントページが最近作られました。
https://ngrx.io/

NgRx Store provides reactive state management for Angular apps inspired by Redux. Unify the events in your application and derive state using Rxjs

Ngexは、Reduxを参考にAngularに状態管理を提供するライブラリです。Fluxの思想に従って、Componentで扱うStateをすべて一箇所のストアで一元管理し、Component間でのstateのやりとりを扱いしやすくします。
そもそもReduxって何?って方はReduxの入門記事を以前書いたのでご参照ください。

Ngrxの要素

前述の通り、NgRxはReduxと構成はほぼ同じです。データフローの簡単な遷移図がこちらです。

スクリーンショット 2018-12-04 18.20.47.png

  • Viewからイベントが発火され、Actionを作成
  • ActionをStoreへdispatchする
  • ReducerがActionを受けて新しいStateを作成&更新
  • Selectorを通りViewへ新しいStateが渡る

それぞれの構成要素をチュートリアルのサンプルコードと共に解説していきます。

Action

Storeのstateを変更したい場合に直接変更は行えず、必ずActinonを作成してStoreへdispatchすることで変更を指示します。

src/app/counter.actions.ts
import { Action } from '@ngrx/store';

export enum ActionTypes {
  Increment = '[Counter Component] Increment',
}

export class Increment implements Action {
  readonly type = '[Counter Component] Increment';
}

Actionはプロパティにユニーク値であるtypeを持ちます。

コンポーネントではActionをStoreへdispatchするために、コンストラクタからstoreを注入しておきます。ユーザーイベントに応じて毎回新規のActionを作成し、storeのdispatchメソッドを呼び出します。

src/app/my-counter/my-counter.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Increment } from '../counter.actions';
export class MyCounterComponent {

  constructor(private store: Store<{ count: number }>) {
  }

  increment() {
    this.store.dispatch(new Increment());
  }
}
src/app/my-counter/my-counter.component.html
<button (click)="increment()">Increment</button>

Store

アプリケーションのstateを保持する場所です。アプリケーション内に一つのみ存在します。
AppModuleにStoreModule.forRootを使いReducerと共に登録します。

src/app/app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter';

@NgModule({
  imports: [StoreModule.forRoot({ count: counterReducer })],
})
export class AppModule {}

Reducer

Reducerは、Actionと現在のstateに応じて新しいstateを作成するピュアなメソッドです。
また、stateの初期値の設定も行います。

src/app/counter.reducer.ts
import { Action } from '@ngrx/store';
import { ActionTypes } from './counter.actions';

export const initialState = 0;

export function counterReducer(state = initialState, action: Action) {
  switch (action.type) {
    case ActionTypes.Increment:
      return state + 1; 
    default:
      return state;
  }
}

selector

selectorは、Storeのstateの必要な部分のみを取得する為のものです。createSelectorのメソッドを使い、各stateを取得するselectorを登録します。

reducers.ts
import { createSelector } from '@ngrx/store';

export interface FeatureState {
  counter: number;
}

export interface AppState {
  feature: FeatureState;
}

export const selectFeature = (state: AppState) => state.feature;
export const selectFeatureCount = createSelector(
  selectFeature,
  (state: FeatureState) => state.counter
);

Effect

Effectは本来のReduxの機能には含まれておらず、redux-thunkやredux-sagaに該当するものです。外部APIとのHTTP通信など非同期処理を行う部分を担う箇所です。
Effectは、StoreへdispatchしたActionをキャッチして処理を行い、新しいActionをdispatchします。

Ngrxでもeffectはstoreのモジュールとは別になっています。

yarn add @ngrx/effects
/effects/auth.effects.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Action } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, of } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';

@Injectable()
export class AuthEffects {
  // Listen for the 'LOGIN' action
  @Effect()
  login$: Observable<Action> = this.actions$.pipe(
    ofType('LOGIN'),
    mergeMap(action =>
      this.http.post('/auth', action.payload).pipe(
        // If successful, dispatch success action with result
        map(data => ({ type: 'LOGIN_SUCCESS', payload: data })),
        // If request fails, dispatch failed action
        catchError(() => of({ type: 'LOGIN_FAILED' }))
      )
    )
  );

  constructor(private http: HttpClient, private actions$: Actions) {}
}

Ngrxの内部実装

さて、Ngrxの概要について言及してきましたが、これをrxでどのように実現しているのか、内部実装を見て理解を深めましょう。

スクリーンショット 2018-12-07 14.11.48.png

storeのdispatchメソッド

dispatchメソッドではactionObseverに対してactionをnextしています。
actionObserverはBehaviorSubjectで、disptchされるactionのストリームです。

store/src/store.ts
dispatch<V extends Action = Action>(action: V) {
 this.actionsObserver.next(action);
}

stateとreducer

state自身はBehaviorSubjectで、actionOvserverをsubscribeしています。pipeしてreducerのストリームから登録されているredeucerをwithLatestFromで取得し、scanでreducerを実行して作られた新しいstateを自身のストリームに流しています。
この時同時にscannedActionsストリームにも新しいstateを流しています。

store/src/state.ts
const actionsOnQueue$: Observable<Action> = actions$.pipe(
    observeOn(queueScheduler)
);
const withLatestReducer$: Observable<
    [Action, ActionReducer<any, Action>]
> = actionsOnQueue$.pipe(withLatestFrom(reducer$));

const seed: StateActionPair<T> = { state: initialState };
const stateAndAction$: Observable<{
    state: any;
    action?: Action;
}> = withLatestReducer$.pipe(
    scan<[Action, ActionReducer<T, Action>], StateActionPair<T>>(
    reduceState,
    seed
    )
);

this.stateSubscription = stateAndAction$.subscribe(({ state, action }) => {
    this.next(state);
    scannedActions.next(action);
});

effect

import { Actions, Effect, ofType } from '@ngrx/effects';

effectモジュールで使用するActionsは、上記でのscannedActionsにあたります。つまり、effectの実行タイミングは、storeがreducerの処理を行って新しいstateに変わった後のタイミングということがわかります。

effects/src/actions.ts
constructor(@Inject(ScannedActionsSubject) source?: Observable<V>) {
    super();

    if (source) {
      this.source = source;
    }
  }

さいごに

Ngrxを導入するかどうかは、アプリの規模や開発人数に依ると思います。
とくにAngularsではserviceがStoreのようにsingletonな存在なので、service内に関連するstateを保持すれば状態管理はそんなに困らないです。
開発人数が増えてstateの更新が煩雑になってきている、テストしづらい、などで状態管理をしっかりしたいと感じた時にNgrxを入れることを考えてみてはどうでしょうか。

76
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
58