5
4

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-12-01

はじめに

RTKQueryのドキュメントを読んで勉強になったので概要、機能についてまとめました。
半分メモ的に書いているので、わかりにくい箇所があるかも知れませんがご了承ください。

RTKQueryとは?

Redux Toolkitに含まれるAdd Onです。データフェッチおよびキャッシュツールで、自分でロジックを書く必要がないです。
Webアプリケーションの多くは、サーバーからデータをフェッチして、データを表示させます。また、そのデータを更新し、更新したデータをサーバーに送信し、クライアント上のキャッシュされたデータをサーバー上のデータと同期させます。
上記のような実装をするときに、以下のようなことを考えロジックを実装する必要がありました。

  • 同じデータに対する重複リクエストの回避
  • UIをより速く感じさせるため非同期の通信処理
  • キャッシュの有効期間の管理

またReactコミュニティでは、「データの取得とキャッシュ」は「状態管理」とは異なると考えました。Reduxのような状態管理ライブラリを使用して、データをキャッシュすることもできますが、ユースケースが異なるため、データフェッチのユースケース専用に構築されたのが、RTKQueryです。

RTKQueryを使うときは?

  • 既存のデータ取得ロジックを簡素化したい
  • 時間の経過に伴う状態の変更履歴を確認できるようにしたい
  • 他のReduxと統合したい

RTKQueryの使い方とは?

これから基本的なRTKQueryの使い方について説明します。

データを取得するカスタムフックを作成

createApiを使って、どのAPIからどんなデータを取得するのかを設定する。

.tsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

type hogeType = {
id: string,
name: string
}

export const hogeApi = createApi({
 reducerPath: 'hogeApi',
 baseQuery: fetchBaseQuery({ baseUrl: 'https://hogefuga/api/' }),
 endpoints: (builder) => ({
   getHogeName: builder.query<hogeType, string>({
     query: (name) => `hoge/${name}`,
   }),
 }),
})

export const { useGetHogeByNameQuery } = hogeApi

storeを作成

キャッシュを管理してくれるように、storeにreducerと、middlewareを追加する

.tsx
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { hogeApi } from './services/hoge'

export const store = configureStore({
  reducer: {
    [hogeApi.reducerPath]: hogeApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(hogeApi.middleware),
})

setupListeners(store.dispatch)

App.tsxなどでstoreをReduxのProviderに設定

.tsx
import * as React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'

const rootElement = document.getElementById('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

コンポーネントでQueryを使用する

以下が、APIからのデータのフェッチ方法でした。

.tsx
import * as React from 'react'
import { useGetHogeByNameQuery } from './services/hoge'

export default function App() {
  const { data, error, isLoading } = useGetHogeByNameQuery('hogefuga')

  return (
    <div className="App">
      {error ? (
        <>Oh no, there was an error</>
      ) : isLoading ? (
        <>Loading...</>
      ) : data ? (
        <>
          <p>{data.id}</p>
          <h1>{data.name}</h1>
        </>
      ) : null}
    </div>
  )
}

キャッシュ動作について

データがサーバーからフェッチされると、RTK クエリはデータを Redux ストアに「キャッシュ」として保存します。同じデータに対して追加のリクエストが実行されると、RTK クエリはサーバーに追加のリクエストを送信するのではなく、既存のキャッシュされたデータを提供します。

queryCacheKeyが実行されると、エンドポイントで使用されるパラメーターがシリアル化され、要求用として内部的に保存されます。同じリクエストを実行する2つのコンポーネントは、キャッシュされた同じデータを使用します。

.tsx
import { useGetUserQuery } from './api.ts'

const UserProfile1() {
  const { data } = useGetUserQuery(1)

  return <div>...</div>
}

const UserProfile2() {
  const { data } = useGetUserQuery(2)

  return <div>...</div>
}

const UserProfile3() {
  const { data } = useGetUserQuery(2)

  return <div>...</div>
}

上記のようなコンポーネントがあり、すべてがマウントされた場合、UserProfile2とUserProfile3では同じエンドポイントで同じクエリパラメータのため、どちらかではキャッシュが利用されます。
サブスクリプションが削除されると(デフォルトは60秒後)、データはキャッシュから削除されます。有効期限は、 API定義全体keepUnusedDataForのプロパティを使用して構成することも、エンドポイントごとに構成することもできます。

refetchする方法

refetchを行った場合、キャッシュを無効にしてデータ取得を行います。
keepUnusedDataForの値を秒単位で指定すると、60秒後(デフォルト)に削除されるサブスクリプションを指定することができます。

.tsx
import { useDispatch } from 'react-redux'
import { useGetPostsQuery } from './api'

const Component = () => {
  const dispatch = useDispatch()
  const { data, refetch } = useGetPostsQuery({ count: 5 })

  function handleRefetchOne() {
    // force re-fetches the data
    refetch()
  }

  function handleRefetchTwo() {
    // has the same effect as `refetch` for the associated query
    dispatch(
      api.endpoints.getPosts.initiate(
        { count: 5 },
        { subscribe: false, forceRefetch: true }
      )
    )
  }

  return (
    <div>
      <button onClick={handleRefetchOne}>Force re-fetch 1</button>
      <button onClick={handleRefetchTwo}>Force re-fetch 2</button>
    </div>
  )
}

頻繁に再fetchする場合

refetchOnMountOrArgChangeを使うと、デフォルトの動作よりもfetchを多くしたいときに使用する。
デフォルトでは、refetchOnMountOrArgChangefalseになっています。
trueにすると、クエリに新しいサブスクライバーが追加されたときに、エンドポイントが常に再fetchします。
またnumverも指定することが可能です。
numberを指定すると、最後に実行されたクエリから指定された秒数が経過すると、常に再fetchされます。

.tsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  // global configuration for the api
  refetchOnMountOrArgChange: 30,
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], number>({
      query: () => `posts`,
    }),
  }),
})
.tsx
import { useGetPostsQuery } from './api'

const Component = () => {
  const { data } = useGetPostsQuery(
    { count: 5 },
    // this overrules the api definition setting,
    // forcing the query to always fetch when this component is mounted
    { refetchOnMountOrArgChange: true }
  )

  return <div>...</div>
}

ウィンドウがフォーカスした際に、再fetchする

refetchOnFocusを使用すると、ウィンドウがフォーカスされた際に、サブスクライブされたすべてのクエリを再fetchします。

.tsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  // global configuration for the api
  refetchOnFocus: true,
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], number>({
      query: () => `posts`,
    }),
  }),
})

setupListenersも必ず、呼び出す必要があります。
これで、storeの設定を変更します。

.tsx
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (gDM) => gDM().concat(api.middleware),
})

// enable listener behavior for the store
setupListeners(store.dispatch)

export type RootState = ReturnType<typeof store.getState>

ネットワークが復活した際

refetchOnReconnectは、ネットワークが復活した際に、サブスクライブされた全てのクエリを再fetchします。

.tsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  // global configuration for the api
  refetchOnReconnect: true,
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], number>({
      query: () => `posts`,
    }),
  }),
})
.tsx
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (gDM) => gDM().concat(api.middleware),
})

// enable listener behavior for the store
setupListeners(store.dispatch)

export type RootState = ReturnType<typeof store.getState>

まとめ

Reduxのオプションを使うと、キャッシュ周りでいろいろカスタムができそうでした。

採用のお知らせ

株式会社Relicでは、フロントエンドエンジニアを積極的に採用中です。
またRelicでは、地方拠点がありますので、U・Iターンも大歓迎です!🙌
少しでもご興味がある方は、Relic採用サイトからエントリーください!

参考

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?