Flux Standard Action の実装について

# Flux Standard Action の実装について

最近、reduxの案件をメインに仕事を色々させて頂いています
その中で案外action, reducer周りの実装で設計が定まっていないという現場がちらほらありました
この記事は今後、案件で実装にや設計で迷っている現場に参画した時提案できるための資料になります



## なぜFlux Standard Actionを使うのか

Flux Standard Action はActionの返り値を type, payload, meta, error に限定することで
redux開発での属人性をなくし、誰が見てもreduxのフローが分かるようにする開発設計です

参考リンク: [React+Fluxで正しく設計するためのFlux見直しガイド](https://aloerina01.github.io/blog/2018-09-14-1#action%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E3%81%9F%E3%81%A3%E3%81%9F2%E3%81%A4%E3%81%AE%E3%81%93%E3%81%A8)


Flux StandardActionを参考に実装したものが下記になります

```ts:actions.ts

const ActionNames = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  NAME_CHANGE: 'NAME_CHANGE'
} as const

export const increment = () => ({
  type: ActionNames.INCREMENT
})

export const decrement = () => ({
  type: ActionNames.DECREMENT
})

export const nameChange = (value:string) => ({
  type: ActionNames.NAME_CHANGE,
  payload: {
    name: value
  }
})

export const actions = { increment, decrement, nameChange }

export type ActionsType = ReturnType<typeof increment | typeof decrement | typeof nameChange>
```

```ts:reducer.ts
import { ActionsType } from '../action'

interface State {
  count: number,
  name: string
}

export const initialState:State = {
  count: 0,
  name: ""
};

const counter = (state:CounterState = initialState, action:ActionsType) => {
  switch (action.type) {
    case 'INCREMENT':

      return {
        ...state,
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    case 'NAME_CHANGE':
        return {
          ...state,
          name: action.payload.name
        }
    default:
      return state
  }
}

export default counter
```

actionからの値は `action.payload` で取得するようにします
ですが、この状態はまだ設計として実装しているだけですので
属人性をなくすには実装者がこのルールに則って作成するというのが前提になります
payloadに値を入れるのを忘れてしまうと簡単に崩れます


## redux-actionsについて

redux-actionsはFlux Standard Action用のライブラリーです
これを使うことで自動的にFlux Standard Actionに則ったactionが作成されます

参考記事: [redux-actionsを使って、reduxの記述で楽をしよう！](https://qiita.com/putan/items/49d3003cb0210f0557b2)

```ts
import { createAction } from 'redux-actions';

export const increment = createAction('INCREMENT');
export const nameChange = createAction('NAME_CHANGE');

increment(); // { type: 'INCREMENT' }
increment(10); // { type: 'INCREMENT', payload: 10 }

nameChange({name: "test"}) // { type: 'NAME_CHANGE', payload: { name: "test" } }

```

`createActions`を使うことで、
Actionの定義をまとめて行うことができます

**redux-actions公式: [api/createaction](https://redux-actions.js.org/api/createaction)**

`createActions`の第一引数にまとめて定義したいactionのオブジェクトを渡します、
第二引数以降は配列になっておりtype(文字列)を指定していきます
複雑なactionは第一引数で、第二引数以降はtypeしか返さない簡単なactionを指定するといいです


```ts
import { createActions } from 'redux-actions'

export const actions = createActions(
  {
    NAME_CHANGE: value => ({ name: value }),
  },
  "INCREMENT",
  "DECREMENT"
)

```

実装したものが下記になります


```ts
import { createActions } from 'redux-actions';

const ActionNames = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  NAME_CHANGE: 'NAME_CHANGE'
} as const

export const actions = createActions(
  {
    [ActionNames.NAME_CHANGE]: (value:string) => ({
      name: `${value}さん`
    }),
  },
  ActionNames.DECREMENT,
  ActionNames.INCREMENT
);
```

reducer側は redux-actionsの `handleActions` を使うことでswitchを使わずにまとめることができます
第一引数にreducerとなるオブジェクト
第二引数に初期値をセットします

```ts
import { handleActions } from 'redux-actions';

export interface State {
  count: number,
  name: string
}

export const initialState:State = {
  count: 0,
  name: ""
};

const reducer = handleActions(
  {
    INCREMENT: (state, action) => ({
      ...state,
      count: state.count + 1
    }),

    DECREMENT: (state, action) => ({
      ...state,
      count: state.count - 1
    }),
    NAME_CHANGE: (state, action) => ({
      ...state,
      name: action.payload.name
    })
  },
  initialState
);

export default reducer
```

redux-actionを使うことでactionが簡単に作成でき、
かつpayloadなどを自動的に指定します

### redux-actionsのデメリット

redux-actionsで簡単にactionを作成することはできますが
実は、型推論ができなくなっています
今までだったら、 `increment`などはtypeとpayloadを返すオブジェクトだったのですが
createActionsでまとめているので `typeof` や `ReturnType`などで型に変換できなくなりました

reducerで型の恩恵が存在しないとpaylodに何が含まれるか判断できなくなります
これはtypescriptを使う上で大きなデメリットになります

## typescript-fsaについて

`typescript-fsa` をつかことで actionに型を定義しつつ、Flux Standard Actionの形式で
actionを作成できます

**github公式ページ : [typescript-fsa](https://github.com/aikoven/typescript-fsa)**

下記はactionの実装方法になります

```ts
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory();

export const increment = actionCreator(ActionNames.INCREMENT);
```

`actionCreatorFactory` を実行してactionCreator(仮)を作成します
redux-actions と同様にTypeを渡すことで自動的にpayloadを含めたactionを作成してくれます

redux-actionsとここで違うのは、redux-actionsの場合
Typeだけを渡したactionに引数を与えると中身がpayloadに格納されていました

しかし、typescript-fsaの場合
引数を与えると型でエラーが発生します
上記のactionは引数を許容せず、Typeだけを返す関数として型が定義されているためです

![スクリーンショット 2020-12-23 13.20.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115192/c1f02ec4-4ea5-cd3f-4655-d40719c9ca31.png)


引数を与えるactionは下記になります


```ts
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory();

export const nameChange = actionCreator<{name: string}>(ActionNames.NAME_CHANGE);
```
actionCreatorに型引数を渡します
これで、 `{name: string}` がpayloadの型として定義されました

![スクリーンショット 2020-12-23 13.31.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115192/0eda562c-9083-b6d1-0e04-c7b898571a00.png)



参考リンク: [【TypeScript】Generics(ジェネリックス)を理解する](https://qiita.com/k-penguin-sato/items/9baa959e8919157afcd4)


`typescript-fsa` でactionを作ることでtestコードや、
containerでactionをconnectする際に型推論が行われるため
引数の値を間違えたりすることが抑えられるようになります

下記はreducerで型が合っていない状況です

![スクリーンショット 2020-12-23 13.34.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115192/b2005895-e4f8-bbef-12f7-b335bccaccec.png)


### typescript-fsa のメリット

typescript-fsaでactionを作成することで型が定義でき、
安全なreduxの開発ができます
また、flux-standard-actionに則って開発することになるので
属人性もなくすことができます


最終的なactionとreducerのコードを載せておきます


```ts:action.ts
import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

const ActionNames = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  NAME_CHANGE: 'NAME_CHANGE'
} as const

export const increment = actionCreator(ActionNames.INCREMENT);
export const decrement = actionCreator(ActionNames.DECREMENT);
export const nameChange = actionCreator<{name: string}>(ActionNames.NAME_CHANGE);


export const actions = { increment, decrement, nameChange }

export type ActionsType = ReturnType<typeof increment | typeof decrement | typeof nameChange>
```

```ts:reducer.ts
import { reducerWithInitialState } from "typescript-fsa-reducers";
import { actions } from '../action'
export interface State {
  count: number,
  name: string
}

export const initialState: State = {
  count: 0,
  name: ""
};

const reducer = reducerWithInitialState(initialState)
    .caseWithAction(actions.increment, (state) => ({
        ...state,
        count: state.count +1
      }))
    .caseWithAction(actions.decrement, (state) => ({
      ...state,
      count: state.count -1
    }))
    .caseWithAction(actions.nameChange, (state, action) => ({
      ...state,
      name: action.payload.name
    }));

export default reducer
```