🙆♂️
Redux Toolkit→ React Queryで、コード量が3分の1になった
前提
だいぶ前に書いた記事を上げ忘れていたため、上げています。
そのため、Redux ToolkitのRTK Queryを考慮していません。その点をご了承ください。
目的
APIの状態管理に関するシンプルな実装で、Redux Toolkit(RTK Queryなし)から、React Queryに変更して、どれだけ書きやすくなるのか検証した。書きやすさの指標としてコード量の少なさをみる。
比較
機能の要件
ログイン後にデータ取得して、ゴニョゴニョすることが多いので、その機能を今回の要件とする。具体的には以下の通り。
- ログインが成功すると、そのユーザのID(以後uid)が取得できる
- ログインが成功しuidがnullでなければ、uidを使ってサーバにユーザ情報の問い合わせをする。
- レスポンスであるユーザ情報を状態管理している箇所に保存する
- それをconsole.logに表示する
比較の方針
- Redux ToolkitとReact Queryとで同一機能のコードを書き、どれぐらい記述量が違うか比較する
- 共通なコードの部分は比較しない。差分のみ比較する。
- ログインはログイン成功時にcontextにuserオブジェクトが挿入される前提で今回は比較しない。
- import文を除いたコードの文字数で比較する。
コードの比較部分
コードの全体像
- Redux ToolkitとReact Queryで共通の部分
- 状態管理
- Redux Toolkit
- React Query
- コンポーネント側
- Redux Toolkit
- React Query
Redux ToolkitとReact Queryで共通の部分
- 以下React Queryでも Redux Toolkitでも使っているコード
- アクセスするデータの型やAPIアクセスのためのコード
// ログイン成功時のcontextから提供されるユーザ
type LoginUser = {
uid: string, // user id
}
// ログインユーザのみが参照できるユーザのプライベートな情報 今回のAPIのレスポンス
type UserInfo = {
uid: string, // user id
email: string,
userName: string,
}
// ユーザのプライベートな情報を取得するAPIアクセスの部分 (実装は省略)
// 例: fetcher.userInfo.get({ uid: string }) で、そのユーザのuserInfo情報にアクセスできる
class Fetcher {
userInfo : {
get: {(query: { uid: string }):Promise<UserInfo>},
}
}
export const fetcher = new Fetcher()
// 状態管理のデータにアクセスするためのキー (React Queryのキー、storeのtype)
export const stateKey = {
userInfo: 'userInfo',
}
状態管理
Redux Toolkit
src/@types/state/userInfo.d.ts
- UserInfoのSlice内では、ローディング中とエラーかどうかと取得したデータを型を定義する
// ReduxのUserInfoのSliceの型
type UserInfoSlice = {
isLoading: boolean, // fetch中かどうか
isError: boolean, // リクエストがエラーかどうか
data: UserInfo, // 取得したデータ
}
src/state/userInfo.ts
-
fetch
でデータを取得するAsynkThunkのActionを定義する -
userInfoSlice
で、sliceを定義する-
name
で、slice名をuserInfo
にする -
extraReducers
で、fetchのPromiseの解決によってSliceの変化を定義する
-
-
userInfoActions
で、簡単なaction名でnamespaceを汚さないようにしてからexport
// userInfoをGetで問い合わせて保存するアクション
const fetch = createAsyncThunk(
`${stateKey.userInfo}/fetch`,
async (uid: string)=> await fetcher.userInfo.get({uid})
)
// ReduxのUserInfoのSliceの実装
const userInfoSlice = createSlice({
name: stateKey.userInfo,
initialState: {
isLoading:false,
data: null,
isError: false
} as UserInfoSlice,
reducers: {},
extraReducers: builder => {
// データ取得中
builder.addCase(fetch.pending, state => {
state.isLoading = true
})
// データ取得完了時
builder.addCase(fetch.fulfilled, (state, action)=>{
state.isLoading = false
state.isError = false
state.data = action.payload
})
// データ取得失敗時
builder.addCase(fetch.rejected, state => {
state.isLoading = false
state.isError = true
})
}
})
// アプリ内の保存形式をグローバルに使えるようにするためにexportする
export const userInfoReducer = userInfoSlice.reducer
// グローバルにuserInfoのactionを実行できるようにexportする
export const userInfoActions = {fetch, ...userInfoSlice.action}
src/state/app/store.ts
-
store
を定義する -
RootState
とAppDispatch
の型を定義する
export const store = configureStore({
reducer: {
[stateKey.userInfo]: userInfoReducer
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
src/state/app/hooks.ts
- 型付のdispatchとselectorのhookを定義する
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
React Query
src/hooks/userInfo.ts
-
useGetUserInfoQuery
のcutom hookを定義する - useQueryの第一引数で,キャッシュするデータのキーを指定する。
- useQueryの第二引数で,データ取得する関数を指定する。
- useQueryの第三引数で,オプションを指定する。
-
enabled: !!uid
で、uidが取得成功したタイミングでフェッチする。 -
retry: false
で、リクエストに失敗してもretryしない。
-
export const useGetUserInfoQuery = (uid:string) => {
return useQuery(
stateKey.userInfo,
async ()=> await fetcher.userInfo.get({uid}),
{enabled: !!uid, retry: false}
)
}
コンポーネント側
Redux Toolkit
src/App.tsx
export default function App() {
return (
<Provider store={store}>
<UserLog />
</Provider>
)
}
src/compornents/UserLog.tsx
- ログインが成功して、
useContext
からuser
が取得できたら、useEffect
データを取得する - データが取得が完了したら、ログに流す
export const UserLog:React.VFC = () => {
// ログインが成功すると、user.uidが取得できるようになる
const {user} = useContext(AuthContext)
const userInfoSelector = useAppSelector(state => state.userInfo)
const dispatch = useAppDispatch()
// uidを取得したら、リクエストする。 !!!!react-queryでは、定義しないで済む
useEffect(() => {
if(user && user.uid){
dispatch(userInfoActions.fetch(user.uid))
}
}, [user])
// データを表示
useEffect(() => {
if(!userInfoSelector.isLoading && userInfoSelector.data){
console.log('[userInfo] is fetched from redux')
console.log(userInfoSelector.data)
}
}, [userInfoSelector.isLoading])
// 以下省略
}
React Query
src/App.tsx
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<UserLog />
</QueryClientProvider>
)
}
src/compornents/UserLog.tsx
-
useGetUserInfoQuery
内で、uidが取得できたら、データを取得する。取得後、キャッシュされ、userInfoQuery
からアクセスできる
export const UserLog:React.VFC = () => {
// ログインが成功すると、user.uidが取得できるようになる
const {user} = useContext(AuthContext)
const userInfoQuery = useGetUserInfoQuery(user?.uid)
// データを表示
useEffect(() => {
if(!userInfoQuery.isLoading && userInfoQuery.data){
console.log('[userInfo] is fetched from useQuery')
console.log(userInfoQuery.data)
}
}, [userInfoQuery.isLoading])
// 以下省略
}
比較結果
- 文字数算出ツールを使ってコード量の比較
- Redux Toolkit → 2357文字 / 121行 / 5ページです!
- React Query → 793文字 / 44行 / 2ページです!
結論
- React Queryは、APIの状態管理について書くと、Redux Toolkitより、記述量が1/3ぐらいですむ
- React Queryだと、APIアクセス時などのデータのキャッシュ処理の記述が減った。
- isErrorや、isLoadingなどのPromiseに依存した状態処理をライブラリ内部で済ませててくれた
- React Queryだと、useEffectでリクエストをする記述が省けた。
- useQueryのenabledを設定するだけで、リクエストのタイミングを簡単に記述
- React Queryだと、globalに定義する処理が減る。
- useQueryでキーを設定すればいいだけで、それだけでアプリ内で共通になる
Discussion