この記事について
RTKQueryの設定方法と基本的な使い方についてまとめます。
設定については自分は0から対応していないので、先輩が設定してくれた内容を自分なりに調べたので備考です。
RTKQueryとは?
RTKQueryとは、DataFetchingとCachingToolです。
ReduxToolkitに内蔵されているOptionalなAddOn(付属ツール)になります。
なので、ReduxToolkitをインストールする必要があります。
Reduxと一緒に使うかはプロジェクト次第ですが、せっかくなら一緒に使う方が便利です。
私の勝手なイメージは、状態管理としてキャッシュ系ツールを使う場合、下記組み合わせが結構主流なのかなと思っています。
(違ってたらすみません)
UIの状態管理 & データの状態管理
- ReactHooks(useContext × useReducer) & ReactQuery / SWR
- Redux & RTKQuery
RTKQueryの特徴
- redux toolkitのglobalStateの扱いとは異なるstoreにデータを保存(as a 'cache')
- Bundle Sizeもかなり小さめ(オフィシャルでアピールしています)
- Apollo Client, React Query, Urql, and SWRから影響を受けて、独自のAPIデザイン
RTKQueryは比較的最近登場したツールです
2021年6月にRedux Tookit v1.6.0 でRTKQueryが正式に追加されました
設定
基本的に、こちらのBasic Usageを参考にしつつ、適宜設定をカスタマイズしていきます。
Create an API Slice
RTKQueryのベース設定です。
redux-toolkitに入ってるので新たなライブラリは不要です。redux-toolkitだけ入っていればOK。
createApi
をredux-tookitのpackageからインストールし、APIを設定します。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { RootState } from '@/rtk/store'
export const serverApi = createApi({
reducerPath: 'serverApi',
baseQuery: fetchBaseQuery({
// envで指定
baseUrl: process.env.NEXT_PUBLIC_SERVER_BASE_URL,
// Headerに情報が必要であれば設定
prepareHeaders: (headers, { getState }) => {
const accessToken = (getState() as RootState).auth.access_token
headers.set('accept', 'application/json')
if (accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`)
}
return headers
}
}),
tagTypes: [],
endpoints: () => ({})
})
- reducerPath: Storeキャッシュ用のユニークなkey
- baseQuery:baseURLごとにAPISliceを作ることが推奨されている
- prepareHeaders : optionalなので必要であれば
- tagTypes : 使いシーンがあれば(後述)
- endpoints : serverに対しての操作。query(fetch Data)かmutation(send Data)
injectEndpoints
公式のチュートリアルでは、直接endpointsを指定していますが、
実際のプロジェクトでは、endpoint毎にファイルを分けたいので、こちらを使います。
公式
// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
// 直接指定している
endpoints: (builder) => ({
getPokemonByName: builder.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})
プロジェクト
import { serverApi } from '@/rtk/services/cms/base-cms-api'
// 省略
// 別ファイルで、作成したbaseApiのkey.injectEndpoints
export const favoriteArticles = serverApi.injectEndpoints({
endpoints: (builder) => ({
getFavoriteArticles: builder.query<
Record<'data', FavoriteArticlesType[]> &
Record<'meta', FavoriteArticlesMetaType>,
number
>({
query: (page) => ({
url: `favorite-articles?page=${page}`
}),
providesTags: ['FavoriteArticles']
}),
})
overrideExisting: false
})
- overrideExisting : endpointの定義した中で名前に重複がないかをwarningとして教えてくれます。trueにするとwarningが表示されなくなるのでfalseに。
Configure the Store
先ほど作成したcreateAPIは、"slice reducer"を既に作ってくれているのでstoreに追加します。
併せて、middlewareも設定することで、キャッシングやrtkQueryの便利機能が使えるよになります。
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { authReducer } from '@/rtk/features'
import { serverApi } from '@/rtk/services/server'
import { ALL_CLEAR } from '@/rtk/actions/clear'
export const rootReducer = combineReducers({
// reducerはここに追加していく
auth: authReducer,
// apiのreducerを追加
[serverApi.reducerPath]: serverApi.reducer
})
export const store = configureStore({
reducer: (state, action) => {
// ログアウト用
if (action.type === ALL_CLEAR) {
return rootReducer(undefined, action)
}
// ログイン中
return rootReducer(state, action)
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat([serverApi.middleware])
})
// TS用の設定
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
setupListeners(store.dispatch)
- reducer : reducerPathとreducerが既に作られているので渡す
- middleware : こちらも作られているので公式通りに設定。fetching, caching, invalidation等を使えるようにする
- setupListeners : optionalだけど、
refetchOnFocus
やrefetchOnReconnect
を使えるようにするために設定
Wrap with the Provider
AppをProviderで囲みます
import { AppProps } from 'next/app'
import { ChakraProvider } from '@chakra-ui/react'
import { customTheme } from '@/theme'
import { store } from '@/rtk/store'
import { Provider } from 'react-redux'
import { UseCheckAuth } from '@/hooks/auth'
import '@/lib/cognito/config'
const App = ({ Component, pageProps }: AppProps) => {
return (
<Provider store={store}>
<ChakraProvider theme={customTheme}>
<UseCheckAuth>
<Component {...pageProps} />
</UseCheckAuth>
</ChakraProvider>
</Provider>
)
}
export default App
基本の使い方
ページやコンポーネントでquery hookを呼び出して使います
import React from 'react'
import { NextPage } from 'next'
import { Layout } from '@/components/layout'
import { Container } from '@chakra-ui/react'
import { RadioMaterialList } from '@/components/radio'
import { LoadingPage } from '@/components/loading'
import { useGetHogeQuery } from '@/rtk/services/server/hoge/hoge'
const Hoge: NextPage = () => {
// ダミーのparam
const param = {
id: 1,
month: 1
}
// hookを呼び出す
const { data, isLoading } = useGetHogeQuery(param)
return (
<>
{isLoading || !data ? (
<LoadingPage />
) : (
<Layout
hasHeader
headerLayout={'title'}
headerTitle={'hoge'}
historyBackLink={'/'}
>
<Container maxW={'container.sm'}>
<RadioList list={data.data} />
</Container>
</Layout>
)}
</>
)
}
export default React.memo(Hoge)
どのコンポーネントでもquery hookは呼び出せますが、pagesで呼び出すことを基本としています。
データの流れを把握しやすいようにする為ですが、再レンダリングの考慮はアプリが大きいと考慮する必要はあります。
memo化とか・・
下記特徴です
- 最初の
data
はundefinedになります -
isLoading
が便利です - 他にも
error
やisFetching
(再取得中)なども用意されています
便利系
便利なパラメーターや機能を少し紹介します
Parameter
skip
query hookはマウントされるたびに自動でデータを取りにいきます。キャッシュデータがあればそっち。
しかし、このskipを使うことでデータのフェッチをスキップしてくれます。
refetchOnMountOrArgChange
キャッシュされているかは関係なく、必ずrefetchしてくれます。booleanではなく時間も指定できます。
// 表示すべきpopupのデータをGET
const { data: popupGetData, isLoading: popupGetLoading } = useGetPopupsQuery(
currentPath,
{ skip: hasPopupPath, refetchOnMountOrArgChange: true }
)
initiate
UI layerのためにhooksが作られるわけですが、コンポーネント外(関数の中で使いたい場合など)は、initiateが使えます。
dispatch(api.endpoints.getHoge.initiate())
// 最初に記事を取得する関数
const getFirstArticles = async ({ firstFetch }: { firstFetch: boolean }) => {
// 省略
// ここ
const { data } = await dispatch(
articlesApi.endpoints.getSearchedArticles.initiate(
`${createCmsQuery()}&offset=${articles.contents.length}`
)
)
// データがあればstoreに登録して表示
if (data) {
dispatch(
addDisplayArticles({
contents: data.contents,
totalCount: data.totalCount
})
)
}
}
公式にはAdding a subscriptionと書かれていますが、subscriptionが何を表しているのかはよくわかっていません・・(わかる方ぜひコメントください)
キャッシュとして扱いたいのか?という意味だと勝手に思っていますが、違う気もする。
Cache tags
tagsというのが用意されていますが、こちらは一つのエンドポイントでmutationが行われた時に、他のendpointのデータを無効にするという機能です。
コンポーネントでデータが使われている状態であれば、refetchされ、それ以外はキャッシュデータが消されます。
ユースケースとしては、
「あるリストデータが表示されており、その中の〇番目の1つのデータがPOST/PUTされた時に、リストデータを最新にしたい!」
という場合などですね。
設定手順はこちら
- tagTypesに追加します
- invalidatesTagsで指定(mutation側/POST,PUT,DELETE)
- providesTagsを指定(query側/GET)
tagTypes: [
'FavoriteArticles',
],
keepUnusedDataFor: 600,
endpoints: () => ({})
})
export const favoriteArticles = serverApi.injectEndpoints({
endpoints: (builder) => ({
getFavoriteArticles: builder.query<
Record<'data', FavoriteArticlesType[]> &
Record<'meta', FavoriteArticlesMetaType>,
number
>({
query: (page) => ({
url: `favorite-articles?page=${page}`
}),
// 3. providesTagsを指定
providesTags: ['FavoriteArticles']
}),
putFavoriteArticles: builder.mutation<
Record<'data', FavoriteArticlesType[]>,
string
>({
query: (id) => ({
url: `favorite-articles/${id}`,
method: 'PUT'
}),
// 2. invalidatesTagsで指定
invalidatesTags: ['FavoriteArticles', 'FavoriteArticleCount']
})
}),
overrideExisting: false
})
export const {
useGetFavoriteArticlesQuery,
usePutFavoriteArticlesMutation
} = favoriteArticles
こちらは、自動でデータが再取得されるので、気づかずうちにqueryHookが呼び出されていると思わぬバグにつながるので要注意です。
extraReducer
クライアントのglobalStateとして扱いたい時に使えます。
endPointが叩かれたタイミングで、データをstoreに保存してくれます。
createAsyncThunkと似ているようです(私は使ったことないのでよく知りません..)
addMatcherで成功失敗の状態を取得できます。
import { createSlice, createSelector } from '@reduxjs/toolkit'
import { RootState } from '@/rtk/store'
import { articlesCategoryApi, ArticlesCategoryType } from '@/rtk/services/cms'
const initialState: Record<'articles_category', ArticlesCategoryType[]> = {
articles_category: []
}
const articlesCategorySlice = createSlice({
name: 'articlesCategory',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(
articlesCategoryApi.endpoints.getArticlesCategory.matchFulfilled,
(state, { payload }) => {
state.articles_category = payload.contents
}
)
}
})
const stateSelector = (state: RootState) => state.articlesCategory
export const articlesCategorySelectors = {
articlesCategory: createSelector(stateSelector, (state) => state)
}
export const articlesCategoryReducer = articlesCategorySlice.reducer
こちらもendpointが叩かれた時にデータがsotoreに自動で保存されるので、思わぬバグに要注意です。
まとめ
rtkQueryは公式ドキュメントは充実している方だと思うのですが、英語のみです。
もっと日本語の記事が増えたらなーと思い、まとめてみました。
まだまだほんの一部の機能しか使えていないので、もっと試行錯誤していきます。
あとは他のReactQueryやSWRと比較して使ってみたいです。
結論:キャッシュ系ツールはとても便利!