l08084のブログ

技術記事の執筆は、祈りに近い

Angular + Reduxを学ぶ #1 - selectとdispatchデコレータを使う

f:id:l08084:20180218175432p:plain

はじめに

Reactではなく、AngularからReduxに入門するのは茨の道かと思いますが、やっていきます。 今回はReduxを1サイクル回して、配列を更新してコンポーネントで値を取得するところまでやりました。

開発環境

  • Angular: 5.2.3

  • angular-redux/store: 7.1.0

  • TypeScript: 2.5.3

  • angular/cli: 1.6.5

  • OS: darwin x64

  • Node: 8.1.4

Angular + Redux環境構築手順

まず、Angular CLIを使用してAngularプロジェクトを作成します。

# Install Angular CLI
npm install -g @angular/cli

# Use it to spin up a new app.
ng new [適当なAngularのプロジェクト名]
cd [適当なAngularのプロジェクト名]

angular-redux/storeライブラリをインストール

npm install redux --save @angular-redux/store

flux-standard-actionもインストール

npm install redux --save flux-standard-action

これで環境構築は完了です🎉🎉🎉

コード解説

続いて、コードについて解説していきます。

ReduxをAngularに接続する

まず、app.module.tsでRedux Storeの読み込みを行います。

src/app/app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

import { NgReduxModule, NgRedux } from '@angular-redux/store';
import { IAppState, INITIAL_STATE } from '../../state/store';
import { TetrisActions } from '../../state/actions';
import { Store, createStore } from 'redux';
import { rootReducer } from '../../state/reducer';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    NgReduxModule,
  ],
  providers: [
    TetrisActions,
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(ngRedux: NgRedux<IAppState>) {
    ngRedux.configureStore(rootReducer,
      INITIAL_STATE);
  }
}

👆のコードで行なっていること

  1. NgReduxModuleをアプリケーションにインポート
  2. Actionの一つであるTetrisActionsをDIできるようにproviders:に設定
  3. Redux StoreをngRedux.configureStoreでAngularに接続

Storeの作成

続いてStoreで、State(状態)のプロパティとStateの初期状態での値を設定しています。 今回の例では、board: number[][]の値を更新します。

src/state/store.ts:

import { Action } from 'redux';
import { TetrisActions, InitBoardAction } from '../state/actions';

export interface IAppState {
    board: number[][];
    lose: boolean;
    current: number[][];
    currentX: number;
    currentY: number;
}

export const INITIAL_STATE: IAppState = {
    board: null,
    lose: false,
    current: null,
    currentX: null,
    currentY: null
};

Actionの作成

src/state/actions.ts:

import { Injectable } from '@angular/core';
import { Action } from 'redux';
import { FluxStandardAction } from 'flux-standard-action';
import { dispatch, NgRedux } from '@angular-redux/store';
import { IAppState } from './store';

export type InitBoardAction = FluxStandardAction<number[][], void>;

@Injectable()
export class TetrisActions {
  static readonly INIT_BOARD = 'INIT_BOARD';

  callInitBoard = (): void => {
    // 2次元配列に0を代入する
    const board = Array.from(new Array(20), () => new Array(10).fill(0));
    this.initBoard(board);
  }

  @dispatch() initBoard = (board: number[][]): InitBoardAction => ({
      type: TetrisActions.INIT_BOARD,
      payload: board,
      meta: undefined
    })
}

actions.tsでは、callInitBoard()で2次元配列boardの全要素に0を設定した後、 initBoard()を呼び出してActionをディスパッチしています。

Actionのディスパッチには、@dispatch()デコレータを使用していて、 InitBoardActionの定義には、環境構築時にインストールしたflux-standard-actionを使用しています。

Reducerの作成

src/state/reducer.ts:

import { IAppState } from './store';
import { Action } from 'redux';
import { TetrisActions, InitBoardAction } from './actions';

export function rootReducer(
    lastState: IAppState,
    action: Action
): IAppState {
    switch (action.type) {
        case TetrisActions.INIT_BOARD:
        return {
            board: (action as InitBoardAction).payload,
            lose: lastState.lose,
            current: lastState.current,
            currentX: lastState.currentX,
            currentY: lastState.currentY
        };
      default:
        return lastState;
    }
}

reducer.tsでは、アクションから取得した配列の値をStateのboardプロパティに設定した後、stateを返す処理を行なっています。

コンポーネント側でStateを取得する

最後にコンポーネントで現在のboard[][]の値を取得して、出力します。 NgRedux Storeでは、StoreからObservableとして値をselect(選択)することによって、最新の値取得を実現しています。

src/app/app.component.ts:

import { Component, ViewChild, AfterViewInit, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/fromEvent';

import { select, NgRedux } from '@angular-redux/store';
import { TetrisActions } from '../../state/actions';
import { IAppState } from '../../state/store';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {

  @select() readonly board$: Observable<number[][]>;

  private subscription: Subscription;

  constructor(private tetrisAction: TetrisActions) {
    // boardの新しい値を受け取る
    this.subscription = this.board$.subscribe(console.log);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  ngOnInit() {
    // アクション呼び出し
    this.tetrisAction.callInitBoard();
  }
}

app.component.tsでは、👇2つの処理を行なっています。

  1. ngOnInit()でアクションのcallInitBoard()呼び出し

  2. @selectデコレーターを使用することで、board[][]の最新の値を取得し、console.logで出力

👆👆👆のコードを全部書いた時点でアプリを動作させると、値が更新された状態の2次元配列board[][]が出力されているはずです。

f:id:l08084:20180218184042p:plain

関連サイト

github.com

github.com

Angular + Reduxの公式による解説については下記サイトを参照してください。

store/intro-tutorial.md at master · angular-redux/store · GitHub