Redux Toolkit v1.3.0の新機能を使ってみよう

Redux Toolkit v1.3.0 では`createAsyncThunk`と`EntityAdapter`が追加されるようです。
https://github.com/reduxjs/redux-toolkit/pull/374

さっそく試してみましょう。

## サンプル
[https://codesandbox.io/s/ionic-react-redux-toolkit-sample](https://codesandbox.io/s/ionic-react-redux-toolkit-sample-ous9u?file=/src/index.tsx)
![sample](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106886/3ce84880-b5af-8e1c-e5c6-21b3664c0cb1.png)

## createAsyncThunk
非同期に対応したAction Creatorです。
`pending`、`fulfilled`、`rejected`の3つのアクションを生成してくれます。

```src/store/todo/actions/todo.action.ts
import { createAsyncThunk } from '@reduxjs/toolkit';

import { todoService } from '../../../services';

export const fetchAllTodos = createAsyncThunk(
  'todos/fetchAll',
  async (arg: { offset?: number; limit?: number }) => {
    const { offset, limit } = arg;
    const result = await todoService.fetchAll(offset, limit);
    return { todos: result };
  }
);

export const fetchTodo = createAsyncThunk(
  'todos/fetch',
  async (arg: { id: string }) => {
    const { id } = arg;
    const result = await todoService.fetch(id);
    return { todo: result };
  }
);
```

`createAsyncThunk`が生成するアクションはFSA準拠になっていて、`pending`、`fulfilled`、`rejected`でそれぞれ型が違うようです。

```ts
interface PendingAction<ThunkArg> {
  type: string;
  meta: {
    requestId: string;
    arg: ThunkArg; // ここにActionの引数が入る
  };
}

interface FulfilledAction<ThunkArg, T> {
  type: string;
  payload: T; // fulfilled時のみ値が入る
  meta: {
    requestId: string;
    arg: ThunkArg;
  };
}

interface RejectedAction<ThunkArg> {
  type: string;
  error: {
    name?: string;
    message?: string; // new Error('hoge')の'hoge'が入る
    code?: string;
    stack?: string;
  } | any;
  meta: {
    requestId: string;
    arg: ThunkArg;
    aborted: boolean;
  };
}
```

Container Componentで使う場合はこんな感じです。

```ts:src/pages/todo/todo-list/containers/todo-list.container.tsx
import { unwrapResult } from '@reduxjs/toolkit';
import { useDispatch, useSelector } from 'react-redux';

import { todosSelector, fetchAllTodos } from '../../../../store/todo';

type Props = {
  offset: number;
  limit: number;
};

export const TodoListContainer: React.FC<Props> = props => {
  const { offset, limit } = props;
  const todos = useSelector(todosSelector);
  const dispatch = useDispatch();
  // const dispatch: AppDispatch = useDispatch();

  React.useEffect(() => {
    dispatch(fetchAllTodos({ offset, limit }))  // ActionをDispatch
      .then(unwrapResult)
      .then(payload => {
        console.log({ payload });
      })
      .catch(error => {
        console.log({ error });
      });
  }, [dispatch]);

  return <TodoList todos={todos} />;
};
```

`unwrapResult`を使うと成功時（`fulfilled`時）に`.then(...)`、失敗時（`rejected`時）に`.catch(...)`へと処理を分けてくれます。便利ですね。

この例ではThunkActionを受け取った`dispatch()`がPromiseを返すことを期待していますが、そのままでは利用できないため下記のいずれかの工夫が必要です。

1. `Dispatch`の型をオーバーロードする

```ts:@types/redux-thunk/index.d.ts
import { ThunkAction } from 'redux-thunk';

// Dispatch overload for redux-thunk
// https://github.com/reduxjs/redux-thunk/pull/278
declare module 'redux' {
  /*
   * Overload to add thunk support to Redux's dispatch() function.
   * Useful for react-redux or any other library which could use this type.
   */
  export interface Dispatch<A extends Action<any> = AnyAction> {
    <TReturnType, TState, TExtraThunkArg>(
      thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, A>
    ): TReturnType;
  }
}
```
この型定義は`redux-thunk`のmasterにマージされているので、将来的にはいらなくなりそうです。
　
2. `useDispatch`をキャストする

[公式ドキュメント](https://redux-toolkit.js.org/usage/usage-with-typescript#using-the-extracted-dispatch-type-with-react-redux)に書かれてあるように`AppDispatch`を定義して、

```ts:src/store/store.ts
export const store = configureStore(...);
export type AppDispatch = typeof store.dispatch;
// export const useAppDispatch = () => useDispatch<AppDispatch>();
```
`useDispatch`をキャストして使います。

```tsx
const dispatch: AppDispatch = useDispatch();
```

毎回`AppDispatch`をインポートしないといけないので少々面倒ですね。

## EntityAdapter
`@ngrx/entity`とほぼ同じです。

### Entity
エンティティは以下のようなIDとデータのマップです。

```ts
export type EntityId = number | string;
export interface EntityState<T> {
  ids: EntityId[];
  entities: {
    [id: EntityId]: T
  };
}
```

### State
`EntityState`を継承してStateを作成し、`createEntityAdapter`でエンティティ操作用のアダプターを生成します。

```src/store/todo/states/todo.state.ts
import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';

import { Todo } from '../../../models';

export const featureKey = 'todos';

export interface TodoState extends EntityState<Todo> {
  isFetching: boolean;
  selectedId: string | null;
}

export const adapter = createEntityAdapter<Todo>();

export const initialState: TodoState = adapter.getInitialState({
  isFetching: false,
  selectedId: null
});
```

### Selector

EntityAdapterの`getSelectors()`でエンティティ用のセレクタを取得します。
`createSelector`は自動的にメモ化してくれます。

```src/store/todo/selectors/todo.selector.ts
import { createSelector } from '@reduxjs/toolkit';

import { TodoState, adapter, featureKey } from '../states';

const { selectAll, selectEntities } = adapter.getSelectors();

const featureStateSelector = (state: {[featureKey]: TodoState }) => state[featureKey];

const entitiesSelector = createSelector(
  featureStateSelector,
  selectEntities
);

export const isFetchingSelector = createSelector(
  featureStateSelector,
  state => state.isFetching
);

export const selectedIdSelector = createSelector(
  featureStateSelector,
  state => state.selectedId
);

export const todosSelector = createSelector(
  featureStateSelector,
  selectAll
);

export const todoSelector = createSelector(
  entitiesSelector,
  selectedIdSelector,
  (entities, id) => (id ? entities[id] || null : null)
);
```

NgRxの`createFeatureSelector`も欲しいところ。

### Reducer

`ActionReducerMapBuilder`で書く方の`createReducer`を使いましょう。型推論が効きます。

エンティティの操作はEntityAdapterのヘルパーを使います（[ドキュメント](https://github.com/reduxjs/redux-toolkit/blob/v1.3.0-integration/docs/api/createEntityAdapter.md#crud-functions)）。

- setAll
- upsertOne
- updateOne
- removeOne

※ NgRxと違ってEntityAdapterのメソッドの引数の順番が逆になっている点に注意

```src/store/todo/reducers/todo.reducer.ts
import { createReducer } from '@reduxjs/toolkit';

import { initialState, adapter } from '../states';
import { fetchAllTodos, fetchTodo } from '../actions';

export const reducer = createReducer(initialState, builder =>
  builder
    .addCase(fetchAllTodos.pending, state => {
      return { ...state, isFetching: true };
    })
    .addCase(fetchAllTodos.fulfilled, (state, action) => {
      const { todos } = action.payload;
      return adapter.setAll({ ...state, isFetching: false }, todos);
    })
    .addCase(fetchAllTodos.rejected, state => {
      return { ...state, isFetching: false };
    })
    .addCase(fetchTodo.pending, (state, action) => {
      const { id } = action.meta.arg;
      return { ...state, isFetching: true, selectedId: id };
    })
    .addCase(fetchTodo.fulfilled, (state, action) => {
      const { todo } = action.payload;
      return adapter.upsertOne({ ...state, isFetching: false }, todo);
    })
    .addCase(fetchTodo.rejected, state => {
      return { ...state, isFetching: false };
    })
);
```

Immerが入ってるので`state`に直接代入できますが、Reducer内だけ書き方が変わるのは一貫性が崩れるので個人的には非推奨です。（:thinking: 治安の悪いチームには必要かも？）

## State

特に変わらず。

キー名（`todos`等）はSelectorと合わせる必要があるので変数で持つのを強く推奨します。

```ts:src/store/store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';

import * as todo from './todo';

// Reducers
const reducer = combineReducers({
  todos: todo.reducer,
  //[todo.featureKey]: todo.reducer
});

// Store
export const store = configureStore({ reducer });

// Types
export type State = ReturnType<typeof reducer>;
export type AppDispatch = typeof store.dispatch;
```

ActionのPayloadに`Date`型が入るなどシリアライズ不可な場合は↓のようにチェックを外した状態でミドルウェアを登録します。

```ts
const middleware = getDefaultMiddleware({ serializableCheck: false });
export const store = configureStore({
  reducer,
  middleware
});
```

## Slice

今回は使いませんでした。
（ducksパターンのごちゃ混ぜ具合が怖い:fearful:）

使う場合はコード量だけでなく型推論が効くか、テストをどう書くか等を調べる必要がありそうです。

## まとめ

`createAsyncThunk`と`EntityAdapter`は良さそうでした。

- 公式提供の安心感
- 型による保守性
- 実装で迷わないオピニオン

今ならcreate-react-appのテンプレートにもRedux Toolkitを使ったものが追加されているので気軽に試せるかと思います。

```bash
$ npx create-react-app my-app --template redux
```

Action、SelectorとRedux用のHooksのインポートが煩雑な場合は、コメント欄にあるようにFacade等でStore周りをラップして使うなどの工夫をするのも良いかもしれません。

去年ThunkActionの型解決で苦労したので、今年はRedux Toolkitを推していきたいです。