9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React #2Advent Calendar 2019

Day 23

reduxで面倒なactionの型まわりを楽する方法

Last updated at Posted at 2019-12-22

Redux+TSを書くときに個人的によくやるパターンを書いてみます

頑張って前日に書きました:sob:

はじめに

Reduxの流れ自体は単方向でグルグル回る感じになっています 画像引用元

redux_store_states.png

ReduxのTSに関しては単方向ではなく、Actionの型情報をComponentとReducerに渡す必要があり、ちょっと都合が違います。

スクリーンショット 2019-12-22 16.53.07.png

これをある程度楽する方法を書いていきます

書くもの概要

ユーザーの一覧の表示・追加・削除できる画面

雰囲気はこんなので
スクリーンショット 2019-12-22 17.23.08.png

以下ソース

model
モデル
// ユーザーのモデル
interface IUser {
  id: number
  name: string
  email: string
  age: number
}

Action
action
// アクションのTypeのあれ
export enum UserActions {
  ADD = 'user/ADD',
  REMOVE = 'user/REMOVE',
}

// アクションの一覧
export const userActions = {
  add: (payload: {user: User}) => ({
    type: UserActions.ADD as const,
    payload,
  }),
  remove: (payload: {userId: User['id']}) => ({
    type: UserActions.REMOVE as const,
    payload,
  }),
}

// 以下UtilityType

// MapをUnionとしてとりだす
type MapToUnion<T> = T[keyof T];

interface Action {
  type: string;
}

interface Actions {
  [key: string]: (...args: any) => Action;
}

// userActionsをUnionとして取り出すよう
type ActionToUnion<T extends Actions> = Extract<
  ReturnType<MapToUnion<T>>,
  Action
  >;

// userActionsを {[k: アクション名]: アクションの戻り値} にするやつ
type ActionToMap<T extends Actions> = { [P in keyof T]: ReturnType<T[P]> };

export type UserReducerAction = ActionToMap<typeof userActions>;
export type UserReducerActions = ActionToUnion<
  typeof userActions
  >;

Component
component
interface mapStateToProps {
  users: User[]
}

interface dispatchProps {
  // ここで作った型を指定
  add: (payload: UserReducerAction['add']['payload']) => void
  remove: (payload: UserReducerAction['remove']['payload']) => void
}

type Props = mapStateToProps & dispatchProps

// コンポーネントに関しては特に書かない
const _UserPage: FC<Props> = ({users, addUser, removeUser}) => (
  <UserList
    // ユーザー一覧を受け取って表示
    users={users}
    // 作成時のイベント
    onCreate={(user: User) => addUser({user})}
    // 削除時のイベント
    onRemove={(user: User) => {removeUser({userId: user.id})}}
  />
)

// ストアのイメージ
interface Store {
  user: {
    users: User[]
  }
}

const mapStateToProps =(state: Store): mapStateToProps => ({
  users: state.user.users,
})


const mapDispatchToProps = (dispatch: Dispatch<UserReducerActions>): dispatchProps => ({
  addUser: payload => dispatch(userActions.addUser(payload)),
  removeUser: payload => dispatch(userActions.removeUser(payload)),
})
})

export const UserPage = connect(
  mapStateToProps,
  mapDispatchToProps
)(_UserPage);
Reducer
reducer
interface UserState {
  users: User[]
}

const initialState: UserState = {
  users: [],
}

// UserReducerActions これでActionの型を指定する
export const userReducer = (state = initialState, action: UserReducerActions): UserState => {
  switch (action.type) {
    case UserActions.ADD: {
      return {
        ...state,
        users: state.users.concat(action.payload.user)
      }
    }
    case UserActions.REMOVE: {
      const index = state.users.findIndex(v => v.id === action.payload.userId)
      if (index === -1) {
        return state
      }

      return {
        ...state,
        users: state.users.splice(index, 1);
      }
    }
    default:
      return state
  }
}


解説

肝は

  • アクションをオブジェクトにまとめる https://qiita.com/eretica/private/81e8567756a0f98a3393#action
  • ActionToMapActionToUnion を利用して扱いやすい型にする
  • 以降はComponentやReducerで適宜利用する
  • redux-thunkredux-saga などでもconnectの部分以外は同じようなパターンで利用できる。使う際は dispatch(userActions.add()) で利用可能

UtilyTypeで使いやすい形にすることで、楽できるよ!ってのが伝わると嬉しい

interface dispatchProps {
  addUser: (payload: UserReducerAction['add']['payload']) => void
  removeUser: (payload: UserReducerAction['remove']['payload']) => void
}

よりみち

payload: Action['hoge']['payload'] が冗長であれば

interface dispatchProps {
  addUser: (payload: UserReducerAction['add']['payload']) => void
  removeUser: (payload: UserReducerAction['remove']['payload']) => void
}

こういうUtilityTypeをつくればもう少し楽できます


type ArgumentTypes<F extends Function> = F extends (args: infer A) => any ? (arg: A) => void : never;
type ActionToArguments<T extends Actions> = { [P in keyof T]: ArgumentTypes<T[P]>};
export type UserReducerActionArgument = ActionToArguments<typeof userActions>;

interface dispatchProps {
  addUser: UserReducerActionArgument['add']
  removeUser: UserReducerActionArgument['remove']
}

まとめ

  • actionを使いやすい型にしておくことで、楽をしたい

よりみち2

こちらの記事(なぜカスタムフックを作るのか)のように、custom hooks が状態やアクションを提供する形になればコネクトの部分の型定義は不要になるので、さらに楽できるのではないかと思っています。

個人的にはこのようなcustom hooksはcontainerコンポーネントに相当するものだけが利用(use***)し、従来通りのコンポーネント設計に留めて置くのが吉なのかなとは思っています

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?