「redux-thunkはいらない」について
https://you-may-not-need-thunk.naturalclar.now.sh
TwitterのTLに流れてきたので読んでみた。非常に良い資料だった。
おそらく多くの人が非同期周りのアクションについてはモヤモヤをもって開発していると思う(もちろん自分もそう)。
非同期周りについては一時期 typescript-fsa ( https://github.com/aikoven/typescript-fsa ) を使っていたがメンテがされなくなったので使うのをやめていた。
最近はActionについては独自に型を作ってやっている。
code:typescript
type RequestType<SuccessResult = undefined, ErrorResult = { message: string }> =
| { status: 'none' }
| { status: 'running' }
| { status: 'success'; result: SuccessResult }
| { status: 'error'; result: ErrorResult }
type ActionType<AC, RAC = {}> = ReturnType<
ACkeyof AC | RACkeyof RAC'start' | RACkeyof RAC'success' | RACkeyof RAC'fail'
type Action = ActionType<typeof ActionCreator, typeof RequestActionCreator>
export const ActionCreator = {
setUser: (payload: { user: User }) => ({ type: "USRS/SET_USER", payload })
}
const RequestActionCreator = {
fetchUsers: {
start: () => ({ type: 'USERS/FETCH_USERS#START' as const }),
success: (result: { users: User[] }) => ({
type: 'USERS/FETCH_USERS#SUCCESS' as const,
result
}),
fail: (result: { message: string }) => ({ type: 'USERS/FETCH_USERS#FAIL' as const, result })
},
}
export const AsyncActionCreator = {
fetchUsers: (): AsyncActionType => async (dispatch, getState) => {
const currentRequest = getState().users.requests.fetchUsers
if (currentRequest.status === 'running') return
const { start, fail, success } = RequestActionCreator.fetchUsers
try {
dispatch(start())
const users = await getUsers()
dispatch(success({ users }))
} catch (e) {
dispatch(fail({ message: 'エラー' }))
}
},
}
資料にあったような仕様だとおそらくredux-thunkは無くても全然問題無い。正直ローディング状態だけローカルに持つ方法は自分もやったことがある(結局Reduxが持つ方に戻ったが…)
自分がredux-thunkが必要だと思うのはmiddlewareという点だ。
例えば、上記の fetchUsersの中のgetUsers がもし何かしらの引数を取るようなAPIだった場合を考える。
ユーザー認証を必要とするAPIとかだとトークンを必要とする場合があるのでそれを例にする。
redux-thunkを使わないという前提でそのような場合は、以下のようにアクションを発行するコンポーネントがトークンを渡すことになると思う。
code:typescript
// 適当だけど多分こんな感じ
// Reduxからデータを取る層をconnect時期はContainerと呼んでいたのでとりあえずこれも便宜上Containerと呼んでおく。
export const useCustomHooks = () => {
const token = useSelector(useCallback(({ user }) => user.token, []))
return {
onSubmit: useCallback(() => {
const users = await getUsers(token)
}, token)
}
}
このような場合にコンポーネントの描画に影響せずonSubmit時にしか参照しないtokenをこのContainerが常に参照している。
自分としてはContainerは描画に必要で変更を監視したいReduxのデータを選別するものだと思っているので、描画に不要な値を参照するのは自分の考えるContainerの役割としてはどうなのだろうという気持ちがある(参照することによって描画に関係なくてもrenderが走るようになるのでその分のパフォーマンス向上も込める。どれくらい影響があるかはわからないが…)。
redux-thunkを使う場合はアクション発行時のReduxのデータを getState で参照できるのでアクションの時だけ必要がデータをContainerが参照する必要がない(というかgetState, extraArgument を使わないならredux-thunkは本当に必要無くて、非同期処理する関数にdispatchを引数で渡すのと変わらない。)。
code:typescript
export const useCustomHooks = () => {
const dispatch = useDispatch()
return {
onSubmit: useCallback(() => {
dispatch(AsyncActionCreator.fetchUers())
}, dispatch)
}
}
export const AsyncActionCreator = {
fetchUsers: (): AsyncActionType => async (dispatch, getState) => {
const token = getState().user.token
const users = await getUsers(token)
middlewareの利点はContainer外でReduxに触れる点なのでこれは上記のような場合かなり有効だと思っている。
上記のようなことはよく自分が開発しているとよくあることで、他にも複数のページを跨いで編集をして最後にSubmitするような(Googleのログインみたいにメールアドレス入力画面→パスワード画面など)仕様などでも同じで、Submitのアクションを定義するContainerは常に今までの入力値を参照しているとおもう。
もしアクションボタンがヘッダーにあってヘッダー自体を別のコンポーネントとして切り出してそれようのContainerを定義する場合は描画に全く関係ないデータを常に参照する必要がある。割とコンポーネントを小さくしようとすると起きてくる問題だ。
上記のような理由から自分はredux-thunkを使っているが、他のエンジニアがこのあたりredux-thunk無し(middleware無し)でどやって解決しているのかが気になっている。これが正解とも思わないし、redux-thunk不要説は結構聞くので本当にみんなどうしているのか知りたい…
ちなみにローディング状態をReduxに持つ方に戻ったわけは案外アクションを発行したコンポーネント以外でローディングの状態が必要になることが多かったから。
#redux #react #typescript