ngrxのaction/reducer/effectの定義をシンプルにする

# はじめに
`Angular`と組み合わせて`ngrx`を使うと便利だ、という記事をあちこちで見かけるので使ってみようと思ったのですが、独特な「`Action`に1つ1つ文字列を割り当てて、クラス化する」という文化にぎこちなさを感じていました。([ngrx/platform actions参照](https://github.com/ngrx/platform/blob/master/docs/store/actions.md#typed-actions))
> これは`Typed Action`というもので、Typescriptの型チェックの機能を使って、`Action`が持つパラメータ(`Payload`)を入力補完できるようにするために使われています。

よく見かけるカウンタの例を見てみると分かる通り、`Action`名を変えたかったらあちこちに影響が波及するし、独特な作りがあるので`Action`/`Reducer`のboilerplate(テンプレ)による自動生成を使おう、と言われていたりします。それでも同じ文字列をコピペしまくるのはちょっと…今どき感がない…?

調べてみると同じことを感じている人がいて、色々な工夫をしているのを見かけます。ただ、`ngrx`本家への提案([reducers/actions as classes, no magic strings, reduced boilerplate](https://github.com/ngrx/platform/issues/1221))のように、`ngrx`が元々目指している「何が起きたか(`action`)と状態がどうなるかの分離」をしておらず却下されていたり、`Action`クラスを1つ1つ定義するのは変わらないなど、ちょっと癖があるようでした。

* ngrx-actions: https://github.com/amcdnl/ngrx-actions
* Simplr: https://github.com/ovrmrw/ngrx-store-simplr
* ngrx issue: https://github.com/ngrx/platform/issues/1221

そこで、`ngrx`の思想に準拠し、トリッキーすぎる仕組みを使わず、`action`/`reducer`/`effect`の定義を簡単にする仕組みを考えたので紹介します。([ngrx-utility](https://github.com/stm32p103/ngrx-utility))

# ngrx-utilityの紹介
([githubのリンクはこちら...サンプルもあります](https://github.com/stm32p103/ngrx-utility)) 従来のカウンタの例に倣い、以下のように`action`/`reducer`/`effect`の定義をしてみます。

* Action
    * `increment`(カウンタを進める)
    * `decrement`(カウンタを戻す)
    * `preset`(指定した値にする)
* Reducer
    * `increment`/`decrement`/`preset`で状態`count`を進める/戻す/指定した値にする
* Effect
    * `preset`に対してコンソールに設定値を表示する

## Actionの定義
`@ToAction`というデコレータでstatic methodを修飾するだけです。これだけでもすっきりした気がするので、個人的には満足です。
<center>![image.png](https://qiita-image-store.s3.amazonaws.com/0/296170/20e3f97c-edf5-8eac-5835-4f8b56dcd759.png)</center>

```typescript
export class CounterActions {
    @ToAction() static increment(): any {}
    @ToAction() static decrement(): any {}
    @ToAction() static preset( n: number ): any { return n }
}
```
## Reducerの定義
`ReducerFactory<T>`クラス(Tは状態の型)をインスタンス化し、`add`メソッドにより`action`に対する`reducer`を追加していきます。最終的に、`create`メソッドにより初期値を指定して`reducer`を生成します。本家と比べるとすっきり書けていると思います。

```typescript
const factory = new ReducerFactory<number>();

// add( action, reducer: ( state: T, payload?: any ) => T );
factory.add( CounterActions.increment, ( count: number ) => count + 1 );
factory.add( CounterActions.decrement, ( count: number ) => count - 1 );
factory.add( CounterActions.preset, ( count: number, preset: number ) => preset );

// Define reducer
export const counterReducer = factory.create( 0 );  // initial value = 0
```

この方法の嬉しさは以下のようなところにあります。

* `action`は`ToAction`デコレータで修飾した`static method`で指定するため、入力補完が使える。
* `action`という中間生成物の存在を一切気にする必要がない。
    * `reducer`の第2引数は`payload`=`ToAction`デコレータで修飾した`static method`の戻り値のため。
* `reducer`の定義でお決まりの`action.type`毎の分岐も`payload`の取り出しも不要。

> 現時点のコードは`immutable`のことを考えていませんが、きっと障壁はないはず。

## Effectの定義
本来`effect`は状態への作用に対しての副作用(`side effect`)を表すもので[濫用はNG](https://medium.com/@m3po22/stop-using-ngrx-effects-for-that-a6ccfe186399)ですが、例示のため`preset`に対して`effect`を設け、引数である設定値を`console`に表示するようにします。

`Effect`を定義するには、`@ngrx/effects`が提供する`Actions`に対して、通常の`ofType`ではなく`payloadOf`を`pipe`します。これにより、対象の`action`が来た時に`payload`だけを通過させることができるようになっています。

```typescript
@Injectable()
export class CounterEffect {
    constructor( private actions$: Actions ) {}
    @Effect( { dispatch: false } ) preset$ = this.actions$.pipe(
        payloadOf( CounterActions.preset ), // presetアクションのpayloadを後ろに流す
        map( payload => console.log( payload ) )
    );
}
```
## ngrxへの組み込み
特に従来と変わりません。

```typescript

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    StoreModule.forRoot( { count: counterReducer } ),
    EffectsModule.forRoot( [ CounterEffect ] )
  ],
  providers: [  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
```

## ActionのDispatch
こちらも特に従来と変わりません。`store.dispatch`引数に、`ToAction`で修飾した`static method`の戻り値を与えればよいだけです。

```typescript
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { CounterActions } from './counter';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
    count$: Observable<number>;
    constructor( private store: Store<any> ) {
        this.count$ = this.store.pipe( select('count') );
    }
    
    increment() {
        this.store.dispatch( CounterActions.increment() );
    }
    
    decrement() {
        this.store.dispatch( CounterActions.decrement() );
    }
    
    preset() {
        this.store.dispatch( CounterActions.preset( 10 ) );
    }
}
```

# ngrx-utilityの仕組み
ここまで読んでいただいた方の中には、「`action`って文字列/クラスを指定するんじゃなかった?」とか、「`Payload`はどこへ行った?」と思うかもしれません。実はこれらの仕組みは`ngrx-utility`が裏で使うだけになっており、プログラマから見えないようにしています。これにより上記のように`action`/`reducer`/`effect`の定義をシンプルにしています。

## ToActionデコレータ
`Action`を作る部分の面倒ごとを担ってくれるのが、`ToAction`デコレータです。このデコレータにより、`static method`を修飾することで、以下の2つのことを行っています。

1. 元々の`static method`を置き換える
2. 1で置き換えた`static method`に対して、`action`名を対応付ける

### static methodの置き換え
下図のイメージで元々の`static method`を包んで`action`を返すように置き換えます。CounterActionsのpresetの場合、`type`には`preset.CounterActions`、元々のメソッドを実行した戻り値(プリセット値n)を`payload`に持った`action`を返すように置き換えます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/296170/19206d57-1a05-d099-5f83-b08ebfeaa1b4.png)

### static methodとaction名の対応付け
`Action`名をテキストで直接入力するのは入力補完ができないので、入力補完ができる`static method`から`action`名を参照したいので、デコレータ内で対応関係を作ります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/296170/206a17dd-1812-331b-3ec1-c95c3e8fabeb.png)

### ToActionデコレータの中身
メソッドデコレータで、メソッドの本体`descriptor.value`を新しい関数に置き換えることで、`action`を返すように置き換えています[1]。その上で`DICTIONARY`に新しいメソッドをキーとして、`action`名を登録しています[2]。

```typescript
// to store action name...not so good way.
const DICTIONARY = new Map<any,string>();

// wrap static method to return Action with Payload
export function ToAction(): MethodDecorator {
    return function( target: any, propertyKey: string, descriptor: PropertyDescriptor ) {
        // 元々のstatic method
        let original = descriptor.value; 
        
        // action名の設定(method.classにする)
        const name = propertyKey + '.' + target.name;

        // --------------------------------------------------------------------
        // [1] 元々のmethodをActionを返すように置き換え
        descriptor.value = function(): ActionWithPayload {
            const retVal = original.apply( this, arguments );
            const action: ActionWithPayload = {
                type: name,
                payload: retVal
            };
            return action;
        }
        // --------------------------------------------------------------------
        
        // [2] [1]で置き換えたstaic methodにaction名を対応付け
        DICTIONARY.set( descriptor.value, name );
    }
}
```

## ReducerFactoryクラス
`ngrx`では`action`の`type`は`string`であることが期待されているので、`ReducerFactory`クラスでは`action`の`static method`から`action`名を参照し、`action`名をキーとして`reducer`を`reduces`に追加します。

`create`により、登録済みの`reducer`を集約した`reducer`を生成します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/296170/5e40fb32-0def-0021-e2b2-ecc47612e7bc.png)

```typescript
export class ReducerFactory<T> {
    private reducers: { [ action: string ]: Reducer<T> } = {};
    
    add( actions: Function | Function[], reducer: Reducer<T> ) {
        let actionNames: string[];
    
        if( actions instanceof Array ) {
            actionNames = toActionName( actions );
        } else {
            actionNames = toActionName( [ actions ] );
        }
        
        actionNames.map( action => {
            this.reducers[ action ] = reducer;
        } );
    }
    
    create( initialState: T ): ActionReducer<T> {
        const reducers = { ...this.reducers };  // copy
        return ( state: T = initialState, action: ActionWithPayload ) => {
            const reducer = reducers[ action.type ];
            let ret = state;
            
            if( reducer !== undefined ) {
                ret = reducer( state, action.payload );                
            }
            
            return ret;
        }
    }
}
```

## payloadOf pipable operator
従来の`effect`では、`ofType pipable operator`により、`action`が起きたことを検出し、その後で`payload`にアクセスしますが結局二度手間になると思ったので、これらをまとめています。

```typescript
export const payloadOf = ( ...actions: Function[] ) => ( source: Observable<ActionWithPayload> ) => {
    const actionNames: string[] = toActionName( actions );
    return source.pipe( 
        filter( target => ( actionNames.includes( target.type ) ) ),
        map( target => target.payload )
    );
}
```

# おわりに
`ngrx-utility`が、`ngrx`を使おうとして独特の文化から敬遠していた人の役に立てば幸いです。試したい方は[github](https://github.com/stm32p103/ngrx-utility)か、npmから最新版をダウンロードしてください。

```
npm install @stm32p103/ngrx-utility
```

なお、`ngrx-utility`は`ngrx`の思想に完全に準拠しているか自信がなく、すべての使い方を網羅できているか分かりません。ご意見とうありましたらコメントください。[ngrx本家にもこの機能を提案しているので](https://github.com/ngrx/platform/issues/1345)、良ければそちらにもコメントください。

# 参考
* ngrx-actions: https://github.com/amcdnl/ngrx-actions
* Simplr: https://github.com/ovrmrw/ngrx-store-simplr
* ngrx issue: https://github.com/ngrx/platform/issues/1221
* Stop using ngrx/effects for that: https://medium.com/@m3po22/stop-using-ngrx-effects-for-that-a6ccfe186399
* typescript decorator: https://www.typescriptlang.org/docs/handbook/decorators.html
* pipable operator: https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md