Quantcast
Channel: Angular Advent Calendarの記事 - Qiita
Viewing all articles
Browse latest Browse all 25

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

$
0
0

はじめに

この記事は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を入れることを考えてみてはどうでしょうか。


Viewing all articles
Browse latest Browse all 25

Trending Articles