クエリ
概要
これはRTK Queryで最も一般的なユースケースです。クエリ操作は任意のデータフェッチライブラリで実行できますが、一般的には、データを取得するリクエストにのみクエリを使用することをお勧めします。サーバー上のデータを変更したり、キャッシュを無効にする可能性のある操作には、ミューテーションを使用する必要があります。
デフォルトでは、RTK QueryにはfetchBaseQuery
が付属しており、これは軽量なfetch
ラッパーで、axios
などの一般的なライブラリと同様の方法でリクエストヘッダーとレスポンスの解析を自動的に処理します。fetchBaseQuery
が要件を満たさない場合は、クエリのカスタマイズを参照してください。
環境によっては、fetchBaseQuery
またはfetch
を単独で使用する場合、node-fetch
またはcross-fetch
を使用してfetch
をポリフィルする必要がある場合があります。
useQuery
でフックのシグネチャと詳細を参照してください。
クエリエンドポイントの定義
クエリエンドポイントは、createApi
のendpoints
セクション内でオブジェクトを返し、builder.query()
メソッドを使用してフィールドを定義することで定義されます。
クエリエンドポイントは、URL(URLクエリパラメータを含む)を構築するquery
コールバック、または任意の非同期ロジックを実行し、結果を返すqueryFn
コールバックのいずれかを定義する必要があります。
query
コールバックがURLの生成に追加データが必要な場合は、単一の引数を取るように記述する必要があります。複数のパラメータを渡す必要がある場合は、単一の「オプションオブジェクト」としてフォーマットして渡してください。
クエリエンドポイントは、結果がキャッシュされる前にレスポンスの内容を変更したり、キャッシュの無効化を識別するための「タグ」を定義したり、キャッシュエントリが追加および削除される際に追加のロジックを実行するキャッシュエントリライフサイクルコールバックを提供したりすることもできます。
TypeScriptで使用する場合、戻り値型と期待されるクエリ引数にジェネリックを指定する必要があります: build.query<ReturnType, ArgType>
。引数がない場合は、引数の型にvoid
を使用してください。
- TypeScript
- JavaScript
// Or from '@reduxjs/toolkit/query/react'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
// The query accepts a number and returns a Post
getPost: build.query<Post, number>({
// note: an optional `queryFn` may be used in place of `query`
query: (id) => ({ url: `post/${id}` }),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (
response: { status: string | number },
meta,
arg
) => response.status,
providesTags: (result, error, id) => [{ type: 'Post', id }],
// The 2nd parameter is the destructured `QueryLifecycleApi`
async onQueryStarted(
arg,
{
dispatch,
getState,
extra,
requestId,
queryFulfilled,
getCacheEntry,
updateCachedData,
}
) {},
// The 2nd parameter is the destructured `QueryCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
updateCachedData,
}
) {},
}),
}),
})
// Or from '@reduxjs/toolkit/query/react'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
// The query accepts a number and returns a Post
getPost: build.query({
// note: an optional `queryFn` may be used in place of `query`
query: (id) => ({ url: `post/${id}` }),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (response, meta, arg) => response.status,
providesTags: (result, error, id) => [{ type: 'Post', id }],
// The 2nd parameter is the destructured `QueryLifecycleApi`
async onQueryStarted(
arg,
{
dispatch,
getState,
extra,
requestId,
queryFulfilled,
getCacheEntry,
updateCachedData,
}
) {},
// The 2nd parameter is the destructured `QueryCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
updateCachedData,
}
) {},
}),
}),
})
React Hooksを使用したクエリの実行
React Hooksを使用している場合、RTK Queryはさらにいくつかの処理を行います。主な利点は、レンダリングが最適化されたフックを取得できることであり、「バックグラウンドフェッチ」と便利な派生ブール値を利用できます。
フックは、サービス定義内のendpoint
の名前に基づいて自動的に生成されます。getPost: builder.query()
を持つエンドポイントフィールドは、useGetPostQuery
という名前のフックを生成します。
フックの種類
クエリ関連のフックは5つあります。
useQuery
useQuerySubscription
とuseQueryState
を合成し、主要なフックです。エンドポイントからのデータのフェッチを自動的にトリガーし、コンポーネントをキャッシュされたデータに「サブスクライブ」し、Reduxストアからリクエストの状態とキャッシュされたデータを読み取ります。
useQuerySubscription
refetch
関数を返し、すべてのフックオプションを受け入れます。エンドポイントからのデータのフェッチを自動的にトリガーし、コンポーネントをキャッシュされたデータに「サブスクライブ」します。
useQueryState
- クエリの状態を返し、
skip
とselectFromResult
を受け入れます。Reduxストアからリクエストの状態とキャッシュされたデータを読み取ります。
- クエリの状態を返し、
useLazyQuery
trigger
関数、クエリ結果、および最後のプロミス情報をタプルで返します。useQuery
に似ていますが、データフェッチのタイミングを手動で制御できます。**注: キャッシュされたデータが既に存在する場合にリクエストをスキップしたい場合に備えて、trigger
関数はpreferCacheValue?: boolean
という2番目の引数を取ります。**
useLazyQuerySubscription
trigger
関数と最後のプロミス情報をタプルで返します。useQuerySubscription
に似ていますが、データフェッチのタイミングを手動で制御できます。**注: キャッシュされたデータが既に存在する場合にリクエストをスキップしたい場合に備えて、trigger
関数はpreferCacheValue?: boolean
という2番目の引数を取ります。**
実際には、useGetPostQuery
などの標準的なuseQuery
ベースのフックがアプリケーションで主に使用されますが、他のフックも特定のユースケースで使用できます。
クエリフックオプション
クエリフックは、2つのパラメータ(queryArg?, queryOptions?)
を期待します。
queryArg
パラメータは、基になるquery
コールバックに渡され、URLが生成されます。
queryOptions
オブジェクトは、データフェッチの動作を制御するために使用できるいくつかの追加パラメータを受け入れます。
- skip - クエリの実行をそのレンダリングで「スキップ」できます。デフォルトは
false
です。 - pollingInterval - クエリが指定された間隔(ミリ秒単位)で自動的に再フェッチされるようにします。デフォルトは
0
(オフ)です。 - selectFromResult - 戻り値のサブセットを取得するためにフックの戻り値を変更し、戻り値のサブセットに対してレンダリングを最適化できます。
- refetchOnMountOrArgChange - クエリにマウント時に常に再フェッチを強制できます(
true
が提供された場合)。同じキャッシュに対する最後のクエリから十分な時間(秒単位)が経過した場合に、クエリに再フェッチを強制できます(数値が提供された場合)。デフォルトはfalse
です。 - refetchOnFocus - ブラウザウィンドウがフォーカスを取り戻したときにクエリに再フェッチを強制できます。デフォルトは
false
です。 - refetchOnReconnect - ネットワーク接続を取り戻したときにクエリに再フェッチを強制できます。デフォルトは
false
です。
すべてのrefetch
関連のオプションは、createApiで設定したデフォルトを上書きします。
頻繁に使用されるクエリフックの戻り値
クエリフックは、クエリクエストの最新のdata
などのプロパティと、現在のリクエストライフサイクルの状態のステータスブール値を含むオブジェクトを返します。以下は、最も頻繁に使用されるプロパティの一部です。すべての戻り値のプロパティの詳しいリストについては、useQuery
を参照してください。
data
- 存在する場合、フック引数に関係なく、最新の戻り値の結果。currentData
- 存在する場合、現在のフック引数に対する最新の戻り値の結果。error
- 存在する場合のエラー結果。isUninitialized
-true
の場合、クエリがまだ開始されていないことを示します。isLoading
-true
の場合、クエリが現在初めてロード中で、まだデータがないことを示します。これは、最初に実行されたリクエストではtrue
になりますが、後続のリクエストではfalseになります。isFetching
-true
の場合、クエリが現在フェッチ中ですが、以前のリクエストからのデータがある可能性があることを示します。これは、最初に実行されたリクエストと後続のリクエストの両方でtrue
になります。isSuccess
-true
の場合、クエリが成功したリクエストからのデータを持っていることを示します。isError
-true
の場合、クエリがerror
状態にあることを示します。refetch
- クエリを強制的に再フェッチする関数。
ほとんどの場合、UIをレンダリングするためにdata
とisLoading
またはisFetching
のいずれかを読み取ることになります。
クエリフックの使用例
PostDetail
コンポーネントの例を以下に示します。
export const PostDetail = ({ id }: { id: string }) => {
const {
data: post,
isFetching,
isLoading,
} = useGetPostQuery(id, {
pollingInterval: 3000,
refetchOnMountOrArgChange: true,
skip: false,
})
if (isLoading) return <div>Loading...</div>
if (!post) return <div>Missing post!</div>
return (
<div>
{post.name} {isFetching ? '...refetching' : ''}
</div>
)
}
このコンポーネントの設定方法は、いくつかの優れた特性を持っています。
- **最初のロード時**のみ「読み込み中...」を表示します。
- 初回ロードは、保留中でキャッシュにデータがないクエリとして定義されます。
- ポーリング間隔によってリクエストが再トリガーされると、投稿名に「...refetching」が追加されます。
- ユーザーが
PostDetail
を閉じ、許可された時間内に再度開いた場合、キャッシュされた結果がすぐに提供され、ポーリングは以前の動作で再開されます。
クエリ読み込み状態
createApi
のReact固有のバージョンによって作成された自動生成Reactフックは、特定のクエリの現在の状態を反映する導出されたブール値を提供します。導出されたブール値は、単一のstatus
フラグでは複数の状態が同時に真になる可能性がある(isFetching
とisSuccess
など)ため、より詳細な情報を提供できるため、status
フラグよりも生成されたReactフックで優先されます。
クエリエンドポイントの場合、RTK Queryは、提供される導出情報の柔軟性を高めるために、isLoading
とisFetching
の間にセマンティックな区別を維持します。
isLoading
は、指定されたフックに対して初めてクエリが実行中であることを指します。この時点ではデータは使用できません。isFetching
は、指定されたエンドポイントとクエリパラメータの組み合わせに対してクエリが実行中であることを指しますが、必ずしも初めてとは限りません。このフックによって以前に行われたリクエスト(前のクエリパラメータを使用している可能性があります)からデータが利用できる場合があります。
この区別により、UI動作の制御が向上します。たとえば、isLoading
を使用して初めてロード時にスケルトンを表示し、isFetching
を使用してページ1からページ2に変更する場合や、データが無効化されて再取得された場合に古いデータをグレーアウトすることができます。
import { Skeleton } from './Skeleton'
import { useGetPostsQuery } from './api'
function App() {
const { data = [], isLoading, isFetching, isError } = useGetPostsQuery()
if (isError) return <div>An error has occurred!</div>
if (isLoading) return <Skeleton />
return (
<div className={isFetching ? 'posts--disabled' : ''}>
{data.map((post) => (
<Post
key={post.id}
id={post.id}
name={post.name}
disabled={isFetching}
/>
))}
</div>
)
}
ほとんどの場合data
を使用することが期待されますが、さらに詳細なレベルを実現できるcurrentData
も提供されています。たとえば、UIにデータを半透明で表示して再取得状態を表す場合、data
とisFetching
を組み合わせて実現できます。ただし、現在の引数に対応するデータのみを表示する場合、代わりにcurrentData
を使用できます。
以下の例では、投稿が初めて取得される場合、ローディングスケルトンが表示されます。現在のユーザーの投稿が以前に取得されていて再取得されている場合(たとえば、ミューテーションの結果として)、UIは以前のデータを表示しますが、データをグレーアウトします。ユーザーが変更されると、以前のユーザーのデータをグレーアウトするのではなく、代わりにスケルトンが再び表示されます。
currentData
を使用したUI動作の管理import { Skeleton } from './Skeleton'
import { useGetPostsByUserQuery } from './api'
function PostsList({ userName }: { userName: string }) {
const { currentData, isFetching, isError } = useGetPostsByUserQuery(userName)
if (isError) return <div>An error has occurred!</div>
if (isFetching && !currentData) return <Skeleton />
return (
<div className={isFetching ? 'posts--disabled' : ''}>
{currentData
? currentData.map((post) => (
<Post
key={post.id}
id={post.id}
name={post.name}
disabled={isFetching}
/>
))
: 'No data available'}
</div>
)
}
クエリキャッシュキー
クエリを実行すると、RTK Queryはリクエストパラメータを自動的にシリアル化し、リクエストの内部queryCacheKey
を作成します。同じqueryCacheKey
を生成する将来のリクエストは、元のリクエストに対して重複排除され、サブスクライブされたコンポーネントからクエリのrefetch
がトリガーされた場合、更新が共有されます。
クエリ結果からのデータの選択
親コンポーネントがクエリをサブスクライブしており、子コンポーネントでそのクエリからアイテムを選択する場合があります。ほとんどの場合、結果が既にわかっている場合に、getItemById
タイプのクエリの追加リクエストを実行する必要はありません。
selectFromResult
を使用すると、パフォーマンスの高い方法でクエリ結果から特定のセグメントを取得できます。この機能を使用する場合、選択されたアイテムの基礎となるデータが変更されない限り、コンポーネントは再レンダリングされません。選択されたアイテムがより大きなコレクション内の1つの要素である場合、同じコレクション内の要素への変更は無視されます。
selectFromResult
を使用して単一の結果を抽出するfunction PostsList() {
const { data: posts } = api.useGetPostsQuery()
return (
<ul>
{posts?.data?.map((post) => <PostById key={post.id} id={post.id} />)}
</ul>
)
}
function PostById({ id }: { id: number }) {
// Will select the post with the given id, and will only rerender if the given post's data changes
const { post } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
post: data?.find((post) => post.id === id),
}),
})
return <li>{post?.name}</li>
}
selectFromResult
の全体的な戻り値に対して浅い等値チェックが行われ、再レンダリングを強制するかどうかが決定されることに注意してください。つまり、戻り値のオブジェクトの値のいずれかの参照が変更されると、再レンダリングがトリガーされます。新しい配列/オブジェクトが作成され、コールバック内で戻り値として使用されると、コールバックが実行されるたびに新しいアイテムとして識別されるため、パフォーマンス上の利点が阻害されます。空の配列/オブジェクトを意図的に提供する場合、コールバックが実行されるたびに再作成することを回避するために、安定した参照を維持するために、コンポーネントの外側に空の配列/オブジェクトを宣言できます。
selectFromResult
// An array declared here will maintain a stable reference rather than be re-created again
const emptyArray: Post[] = []
function PostsList() {
// This call will result in an initial render returning an empty array for `posts`,
// and a second render when the data is received.
// It will trigger additional rerenders only if the `posts` data changes
const { posts } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
posts: data ?? emptyArray,
}),
})
return (
<ul>
{posts.map((post) => (
<PostById key={post.id} id={post.id} />
))}
</ul>
)
}
上記の動作を要約すると、戻り値は正しくメモ化されている必要があります。セレクターを使用したデータの導出およびRedux Essentials - RTK Query高度なパターンも参照して、追加情報を確認してください。
不要なリクエストの回避
デフォルトでは、既存のものと同じクエリを行うコンポーネントを追加した場合、リクエストは実行されません。
場合によっては、この動作をスキップして再取得を強制的に行う必要がある場合があります。その場合は、フックによって返されるrefetch
を呼び出すことができます。
Reactフックを使用していない場合は、このようにrefetch
にアクセスできます。
const { status, data, error, refetch } = dispatch(
pokemonApi.endpoints.getPokemon.initiate('bulbasaur'),
)
例:キャッシング動作の観察
この例では、リクエストの重複排除とキャッシング動作を示しています。
- 最初の
Pokemon
コンポーネントがマウントされ、「bulbasaur」をすぐに取得します。 - 1秒後、「bulbasaur」を使用して別の
Pokemon
コンポーネントがレンダリングされます。- このコンポーネントは「Loading...」を表示せず、新しいネットワークリクエストも発生しないことに注意してください。ここではキャッシュを使用しています。
- その後すぐに、「pikachu」の
Pokemon
コンポーネントが追加され、新しいリクエストが発生します。 - 特定の
Pokemon
に対して「Refetch」をクリックすると、そのPokemon
のすべてのインスタンスが1つのリクエストで更新されます。
「Add bulbasaur」ボタンをクリックします。コンポーネントの「Refetch」ボタンをクリックするまで、上記で説明したのと同じ動作が観察されます。