今更まとめたRTKQuery

# はじめに
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を多くしたいときに使用する。
デフォルトでは、`refetchOnMountOrArgChange`が`false`になっています。
`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採用サイトからエントリーください！

https://relic.co.jp/categories/job_4?utm_source=qiita&utm_medium=article&utm_campaign=qiita_article

# 参考

https://redux-toolkit.js.org/rtk-query/overview

https://qiita.com/7tsuno/items/2301a35283db7cd54df9#refetchonfocus