🙆‍♂️

Redux Toolkit→ React Queryで、コード量が3分の1になった

2021/11/24に公開

前提

だいぶ前に書いた記事を上げ忘れていたため、上げています。
そのため、Redux ToolkitのRTK Queryを考慮していません。その点をご了承ください。

目的

APIの状態管理に関するシンプルな実装で、Redux Toolkit(RTK Queryなし)から、React Queryに変更して、どれだけ書きやすくなるのか検証した。書きやすさの指標としてコード量の少なさをみる。

比較

機能の要件

ログイン後にデータ取得して、ゴニョゴニョすることが多いので、その機能を今回の要件とする。具体的には以下の通り。

  1. ログインが成功すると、そのユーザのID(以後uid)が取得できる
  2. ログインが成功しuidがnullでなければ、uidを使ってサーバにユーザ情報の問い合わせをする。
  3. レスポンスであるユーザ情報を状態管理している箇所に保存する
  4. それを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を定義する
  • RootStateAppDispatchの型を定義する
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