本文へスキップ

クエリ

概要

これはRTK Queryで最も一般的なユースケースです。クエリ操作は任意のデータフェッチライブラリで実行できますが、一般的には、データを取得するリクエストにのみクエリを使用することをお勧めします。サーバー上のデータを変更したり、キャッシュを無効にする可能性のある操作には、ミューテーションを使用する必要があります。

デフォルトでは、RTK QueryにはfetchBaseQueryが付属しており、これは軽量なfetchラッパーで、axiosなどの一般的なライブラリと同様の方法でリクエストヘッダーとレスポンスの解析を自動的に処理します。fetchBaseQueryが要件を満たさない場合は、クエリのカスタマイズを参照してください。

情報

環境によっては、fetchBaseQueryまたはfetchを単独で使用する場合、node-fetchまたはcross-fetchを使用してfetchをポリフィルする必要がある場合があります。

useQueryでフックのシグネチャと詳細を参照してください。

クエリエンドポイントの定義

クエリエンドポイントは、createApiendpointsセクション内でオブジェクトを返し、builder.query()メソッドを使用してフィールドを定義することで定義されます。

クエリエンドポイントは、URL(URLクエリパラメータを含む)を構築するqueryコールバック、または任意の非同期ロジックを実行し、結果を返すqueryFnコールバックのいずれかを定義する必要があります。

queryコールバックがURLの生成に追加データが必要な場合は、単一の引数を取るように記述する必要があります。複数のパラメータを渡す必要がある場合は、単一の「オプションオブジェクト」としてフォーマットして渡してください。

クエリエンドポイントは、結果がキャッシュされる前にレスポンスの内容を変更したり、キャッシュの無効化を識別するための「タグ」を定義したり、キャッシュエントリが追加および削除される際に追加のロジックを実行するキャッシュエントリライフサイクルコールバックを提供したりすることもできます。

TypeScriptで使用する場合、戻り値型と期待されるクエリ引数にジェネリックを指定する必要があります: build.query<ReturnType, ArgType>。引数がない場合は、引数の型にvoidを使用してください。

すべてのクエリエンドポイントオプションの例
// 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,
}
) {},
}),
}),
})

React Hooksを使用したクエリの実行

React Hooksを使用している場合、RTK Queryはさらにいくつかの処理を行います。主な利点は、レンダリングが最適化されたフックを取得できることであり、「バックグラウンドフェッチ」と便利な派生ブール値を利用できます。

フックは、サービス定義内のendpointの名前に基づいて自動的に生成されます。getPost: builder.query()を持つエンドポイントフィールドは、useGetPostQueryという名前のフックを生成します。

フックの種類

クエリ関連のフックは5つあります。

  1. useQuery
    • useQuerySubscriptionuseQueryStateを合成し、主要なフックです。エンドポイントからのデータのフェッチを自動的にトリガーし、コンポーネントをキャッシュされたデータに「サブスクライブ」し、Reduxストアからリクエストの状態とキャッシュされたデータを読み取ります。
  2. useQuerySubscription
    • refetch関数を返し、すべてのフックオプションを受け入れます。エンドポイントからのデータのフェッチを自動的にトリガーし、コンポーネントをキャッシュされたデータに「サブスクライブ」します。
  3. useQueryState
    • クエリの状態を返し、skipselectFromResultを受け入れます。Reduxストアからリクエストの状態とキャッシュされたデータを読み取ります。
  4. useLazyQuery
    • trigger関数、クエリ結果、および最後のプロミス情報をタプルで返します。useQueryに似ていますが、データフェッチのタイミングを手動で制御できます。**注: キャッシュされたデータが既に存在する場合にリクエストをスキップしたい場合に備えて、trigger関数はpreferCacheValue?: booleanという2番目の引数を取ります。**
  5. 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をレンダリングするためにdataisLoadingまたは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>
)
}

このコンポーネントの設定方法は、いくつかの優れた特性を持っています。

  1. **最初のロード時**のみ「読み込み中...」を表示します。
    • 初回ロードは、保留中でキャッシュにデータがないクエリとして定義されます。
  2. ポーリング間隔によってリクエストが再トリガーされると、投稿名に「...refetching」が追加されます。
  3. ユーザーがPostDetailを閉じ、許可された時間内に再度開いた場合、キャッシュされた結果がすぐに提供され、ポーリングは以前の動作で再開されます。

クエリ読み込み状態

createApiのReact固有のバージョンによって作成された自動生成Reactフックは、特定のクエリの現在の状態を反映する導出されたブール値を提供します。導出されたブール値は、単一のstatusフラグでは複数の状態が同時に真になる可能性がある(isFetchingisSuccessなど)ため、より詳細な情報を提供できるため、statusフラグよりも生成されたReactフックで優先されます。

クエリエンドポイントの場合、RTK Queryは、提供される導出情報の柔軟性を高めるために、isLoadingisFetchingの間にセマンティックな区別を維持します。

  • isLoadingは、指定されたフックに対して初めてクエリが実行中であることを指します。この時点ではデータは使用できません。
  • isFetchingは、指定されたエンドポイントとクエリパラメータの組み合わせに対してクエリが実行中であることを指しますが、必ずしも初めてとは限りません。このフックによって以前に行われたリクエスト(前のクエリパラメータを使用している可能性があります)からデータが利用できる場合があります。

この区別により、UI動作の制御が向上します。たとえば、isLoadingを使用して初めてロード時にスケルトンを表示し、isFetchingを使用してページ1からページ2に変更する場合や、データが無効化されて再取得された場合に古いデータをグレーアウトすることができます。

クエリ読み込み状態を使用したUI動作の管理
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にデータを半透明で表示して再取得状態を表す場合、dataisFetchingを組み合わせて実現できます。ただし、現在の引数に対応するデータのみを表示する場合、代わりに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'),
)

例:キャッシング動作の観察

この例では、リクエストの重複排除とキャッシング動作を示しています。

  1. 最初のPokemonコンポーネントがマウントされ、「bulbasaur」をすぐに取得します。
  2. 1秒後、「bulbasaur」を使用して別のPokemonコンポーネントがレンダリングされます。
    • このコンポーネントは「Loading...」を表示せず、新しいネットワークリクエストも発生しないことに注意してください。ここではキャッシュを使用しています。
  3. その後すぐに、「pikachu」のPokemonコンポーネントが追加され、新しいリクエストが発生します。
  4. 特定のPokemonに対して「Refetch」をクリックすると、そのPokemonのすべてのインスタンスが1つのリクエストで更新されます。
試してみてください

「Add bulbasaur」ボタンをクリックします。コンポーネントの「Refetch」ボタンをクリックするまで、上記で説明したのと同じ動作が観察されます。