Redux+TSを書くときに個人的によくやるパターンを書いてみます
頑張って前日に書きました
はじめに
Reduxの流れ自体は単方向でグルグル回る感じになっています 画像引用元
ReduxのTSに関しては単方向ではなく、Actionの型情報をComponentとReducerに渡す必要があり、ちょっと都合が違います。
これをある程度楽する方法を書いていきます
書くもの概要
ユーザーの一覧の表示・追加・削除できる画面
以下ソース
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
-
ActionToMap
とActionToUnion
を利用して扱いやすい型にする - 以降はComponentやReducerで適宜利用する
-
redux-thunk や redux-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***)し、従来通りのコンポーネント設計に留めて置くのが吉なのかなとは思っています