まだredux-toolkit使ってないの?
June 26th, 2020
Introduction
以前あげたredux-sagaの記事でredux-sagaの素晴らしさを書いてみたんですが、sagaは導入ハードルこそ高いですが、複雑な副作用制御に秩序をもたらせる素晴らしいライブラリーです。 ただ逆に言うと、そこまで複雑な副作用制御を必要としない場合、sagaはtoo muchになるとも思います。
そもそも軽く使うには、Reduxって結構色々知らなきゃいけなかったりしますよね。
- デバッグ:redux-devtools-extension
- 設計:ducks, reducks, redux-way
- 非同期処理:redux-thunk, redux-saga
- immutable系:immer, immutablejs
- utility:redux-actions, typescript-fsa
- その他:normalizr, reselect
とまぁ、これらの中で今回のプロジェクトでどれをどう使うか選択しなきゃだし、地味にコストはかかるものです。 だからこそプロジェクト用に自前のRedux入りボイラープレートとか用意する人も多かったのではないでしょうか?
こういった現状を解決しようとReduxの中の人が作ったある種のテンプレートがredux-toolkitです。
redux-toolkit
redux-toolkitとは?
Reduxメンテナの1人、Mark Erikson先生がはじめたプロジェクトで、Reduxのリポジトリにある公式なライブラリです。 前述のように、Redux自体は軽量で限られた部分を担うライブラリのため、関連ライブラリなども豊富なで多くの選択肢やプラクティスが存在します。 なのでそれらを公式がまとめ、最適化したものがこのredux-toolkitです。
create-react-appのreduxのtemplateがあるのですが、その中でもこのredux-toolkitが使われており、また公式サポートなベストプラクティスなので 今後デファクトスタンダードになっていくのではないかと思います。
主な特徴
redux-toolkitは以下のライブラリを内包しています。
- immer
- redux-thunk
- reselect
また直接内包しているわけではないですが、
- redux-devtools-extensionがデフォルトで設定されており、booleanで切り替え可能
- autoduxに由来するslice作成が可能(ducksやre-ducksが短くかける)
といった機能もあり、多くの人がプロジェクトごとに毎回書いてた冗長な記述が不要になっています。
主要なAPI
主要なAPIを簡単に書いておくと、↓こんな感じです。
- configureStore:ReduxのcreateStore周りの設定をいい感じにまとめたもの
- createAction:redux-actions同様、action生成のUtility
- createReducer:reducerの冗長になりがちな記述を短くかけるUtility
- createSlice:autoduxに由来するsliceを生成する、reducer名称を元にactionが自動発行される
- createAsyncThunk:thunkを作成し、「pending」「fulfilled」「rejected」というpostfix付きのactionを発行する
※細かい内容は公式のTutorialを参照ください。
redux-toolkitを実際に使ってみる
ちょっと前置きが長くなりましたが、実際に本サイト(今日時点でContact周りのみReduxを使用)へ適用してみたときのソースをみていきたいと思います。 redux-toolkitを入れる前からducks構成だったので、redux-toolkitのsliceを使用してducks構成のまま適用しました。
Slice
slice周りのソースは↓こんな感じになりました。
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { commentValidate, mailValidate, nameValidate } from '../../utils/contactValidater'
export type UserState = {
name: {
value: string,
error: string,
},
email: {
value: string,
error: string,
},
comment: {
value: string,
error: string,
},
isCompletedSubmit: boolean,
}
type Reducer = {
updateName: (state: UserState, { payload }: PayloadAction<string>) => void
updateEmail: (state: UserState, { payload }: PayloadAction<string>) => void
updateComment: (state: UserState, { payload }: PayloadAction<string>) => void
}
const userSlice = createSlice<UserState, Reducer>({
name: 'user',
initialState: {
name: {
value: '',
error: '',
},
email: {
value: '',
error: '',
},
comment: {
value: '',
error: '',
},
isCompletedSubmit: false,
},
reducers: {
updateName: (state, { payload }) => {
const error = nameValidate(payload)
state.name.value = payload
state.name.error = error
},
updateEmail: (state, { payload }) => {
const error = mailValidate(payload)
state.email.value = payload
state.email.error = error
},
updateComment: (state, { payload }) => {
const error = commentValidate(payload)
state.comment.value = payload
state.comment.error = error
},
},
})
export const {
updateName,
updateEmail,
updateComment,
} = userSlice.actions
export default userSlice.reducer
reducersのキー名がそのままactionとして作成されるので、複雑なactionを生成できないという制限もつれけるし行数も減って良いですね。 reducerに渡すcallbackがimmerが適用されてて、mutableっぽくかけるのがちょっと微妙だなと思ってたんですが、実際使ってみるとやっぱ短くはなるし悪くないですね。 何よりこれくらいの依存ならReduxの三原則さえ理解してれば「actionとかreducerをいい感じにやってくれてるんだ」くらいは初見でもわかるのがいいですね。
(まぁこのサイトは僕以外がいじることはないと思いますが・・・)
Thunk
非同期処理もredux-toolkitのcreateAsyncThunkを使うように修正しました。
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { navigate } from 'gatsby-link'
import { State } from '../../store'
import { commentValidate, mailValidate, nameValidate } from '../../utils/contactValidater'
import { encode } from '../../utils/encode'
type ThunkConfig = {
state: State
rejectValue: {
nameError: string
emailError: string
commentError: string
}
}
export const postContactForm = createAsyncThunk<void, string, ThunkConfig>(
'user/postContactForm',
async (formName, { getState, rejectWithValue }) => {
const {
app: {
user: {
name,
email,
comment,
}
}
} = getState()
const nameError = nameValidate(name.value)
const emailError = mailValidate(email.value)
const commentError = commentValidate(comment.value)
if (nameError || emailError || commentError) {
return rejectWithValue({
nameError,
emailError,
commentError,
})
}
const req = fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: encode({
'form-name': formName,
name: name.value,
email: email.value,
comment: comment.value,
}),
})
.then(() => navigate('/thanks/'))
.catch(error => alert(error))
return await req
}
)
// --snip--
const userSlice = createSlice<UserState, Reducer>({
// --snip--
extraReducers: builder => {
builder.addCase(postContactForm.fulfilled, (state) => {
state.isCompletedSubmit = true
})
builder.addCase(postContactForm.rejected, (state, { payload }) => {
if (payload) {
state.name.error = payload.nameError
state.email.error = payload.emailError
state.comment.error = payload.commentError
}
})
}
})
redux-thunkで非同期処理を書く時って大抵resolveやrejectでそれぞれdispatchしたり、Promise発行時のreducerとthunkにロジックが別れたりするので redux-toolkitではそれをより使いやすくまとめてて良いですね。 これならローダーとか追加したくなっても、thunkの中をいじる必要はないですからね。
まとめ
良かった点
- Redux初心者にもわかりやすい
- Sliceを使ってればducksとか知らない人にも設計理解は容易
- Thunkの冗長性も排除されてて非同期処理も書きやすい
- 全体的に人によって書き方が異なるみたいなのをうまく排除してる
- SSR考慮時のmiddleware周りのだるさから解放される
気になる点
- Sliceを使わずreducerとか作れると部分的に異なる書き方ができてしまう
- immerの落とし穴を考慮しながら実装しなきゃいけなくなるので、immerがデフォルトなのは初心者には優しくない気もする
- Redux単体で使うのがもはやアンチパターンなのかってくらいこういうのが流行ってるけど、何が何でもReduxのCoreは拡張しないのだろうか(いろいろissueで議論されてたっぽいけど)
とまぁ、多少気になるところもありつつ、現状だとメリットの方が多い気がするので今後デファクトになっていくんじゃないかと期待。 あとが最近React公式からRecoilも発表されたので、今後Reactユーザーがどっちを選んでいくのか要注目ですね。