8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RTKQueryの基本(設定〜使い方)

Last updated at Posted at 2022-05-24

この記事について

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を設定します。

base-server-api.ts
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}`,
    }),
  }),
})

プロジェクト

favorite-articles.ts
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の便利機能が使えるよになります。

stores.ts
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だけど、refetchOnFocusrefetchOnReconnectを使えるようにするために設定

Wrap with the Provider

AppをProviderで囲みます

_app.tsx
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を呼び出して使います

pages/hoge/hoge.tsx
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が便利です
  • 他にもerrorisFetching(再取得中)なども用意されています

便利系

便利なパラメーターや機能を少し紹介します

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された時に、リストデータを最新にしたい!」
という場合などですね。

設定手順はこちら

  1. tagTypesに追加します
  2. invalidatesTagsで指定(mutation側/POST,PUT,DELETE)
  3. providesTagsを指定(query側/GET)
base-server-api.ts
  tagTypes: [
    'FavoriteArticles',
  ],
  keepUnusedDataFor: 600,
  endpoints: () => ({})
})
favorite-article.ts
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で成功失敗の状態を取得できます。

article-category-slice.ts
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と比較して使ってみたいです。

結論:キャッシュ系ツールはとても便利!

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?