クエリのカスタマイズ
RTK Queryは、リクエストの解決方法に依存しません。リクエストの処理には任意のライブラリを使用できますし、ライブラリを使用しないこともできます。RTK Queryは、ほとんどのユースケースをカバーする妥当なデフォルトを提供しながら、特定のニーズに合わせてクエリの処理を変更するためのカスタマイズの余地も残しています。
baseQuery
によるクエリのカスタマイズ
クエリを処理するデフォルトの方法は、createApi
のbaseQuery
オプションと、エンドポイント定義のquery
オプションを組み合わせて使用することです。
クエリを処理するために、エンドポイントはquery
オプションで定義され、その戻り値はAPIで使用される共通のbaseQuery
関数に渡されます。
デフォルトでは、RTK QueryにはfetchBaseQuery
が付属しています。これは軽量なfetch
ラッパーで、axios
などの一般的なライブラリと同様に、リクエストヘッダーとレスポンスの解析を自動的に処理します。fetchBaseQuery
だけではニーズを満たせない場合は、ラッパー関数で動作をカスタマイズするか、baseQuery
関数をゼロから作成してcreateApi
で使用できます。
こちらも参照してください baseQuery APIリファレンス
.
カスタムbaseQuery
の実装
RTK Queryでは、baseQuery
関数が3つの引数(args
、api
、extraOptions
)で呼び出されることが期待されます。これは、data
プロパティまたはerror
プロパティを持つオブジェクト、あるいはそのようなオブジェクトを返すpromiseを返す必要があります。
ベースクエリ関数とクエリ関数は、常に自身でエラーをキャッチし、オブジェクトで返す必要があります!
function brokenCustomBaseQuery() {
// ❌ Don't let this throw by itself
const data = await fetchSomeData()
return { data }
}
function correctCustomBaseQuery() {
// ✅ Catch errors and _return_ them so the RTKQ logic can track it
try {
const data = await fetchSomeData()
return { data }
} catch (error) {
return { error }
}
}
baseQuery関数の引数
const customBaseQuery = (
args,
{ signal, dispatch, getState },
extraOptions,
) => {
// omitted
}
baseQuery関数の戻り値
- 成功時の期待される結果形式
return { data: YourData }
- エラー時の期待される結果形式
return { error: YourError }
const customBaseQuery = (
args,
{ signal, dispatch, getState },
extraOptions,
) => {
if (Math.random() > 0.5) return { error: 'Too high!' }
return { data: 'All good!' }
}
この形式は、RTK Queryがレスポンスの戻り値の型を推論するために必要です。
基本的に、baseQuery
関数は、data
またはerror
プロパティを持つオブジェクトという最小限の戻り値のみが必要で有効です。提供された引数の使用方法と、関数内でのリクエストの処理方法は、ユーザーが決定します。
fetchBaseQuery
のデフォルト値
fetchBaseQuery
については、戻り値の型は次のとおりです。
fetchBaseQuery
の戻り値の型Promise<
| {
data: any
error?: undefined
meta?: { request: Request; response: Response }
}
| {
error: {
status: number
data: any
}
data?: undefined
meta?: { request: Request; response: Response }
}
>
fetchBaseQuery
を使用した成功時の期待される結果形式return { data: YourData }
fetchBaseQuery
を使用したエラー時の期待される結果形式return { error: { status: number, data: YourErrorData } }
transformResponse
によるクエリレスポンスのカスタマイズ
createApi
の個々のエンドポイントは、transformResponse
プロパティを受け入れます。これにより、クエリまたはミューテーションによって返されたデータがキャッシュに保存される前に操作できます。
transformResponse
は、対応するエンドポイントに対して成功したbaseQuery
が返すデータで呼び出され、transformResponse
の戻り値はそのエンドポイント呼び出しに関連付けられたキャッシュデータとして使用されます。
デフォルトでは、サーバーからのペイロードが直接返されます。
- TypeScript
- JavaScript
function defaultTransformResponse(
baseQueryReturnValue: unknown,
meta: unknown,
arg: unknown
) {
return baseQueryReturnValue
}
function defaultTransformResponse(baseQueryReturnValue, meta, arg) {
return baseQueryReturnValue
}
変更するには、次のような関数を使用します。
transformResponse: (response, meta, arg) =>
response.some.deeply.nested.collection
transformResponse
は、変換されたレスポンスを決定する際に使用できる、baseQuery
から返されたmeta
プロパティを2番目の引数として呼び出されます。meta
の値は、使用されているbaseQuery
によって異なります。
transformResponse
のmetaの例transformResponse: (response: { sideA: Tracks; sideB: Tracks }, meta, arg) => {
if (meta?.coinFlip === 'heads') {
return response.sideA
}
return response.sideB
}
transformResponse
は、変換されたレスポンスを決定する際に使用できる、エンドポイントに提供されたarg
プロパティを3番目の引数として呼び出されます。arg
の値は、使用されているendpoint
と、クエリ/ミューテーション呼び出しで使用される引数によって異なります。
transformResponse
のargの例transformResponse: (response: Posts, meta, arg) => {
return {
originalArg: arg,
data: response,
}
}
RTK Queryがキャッシュデータを管理しているので、レスポンスを正規化されたルックアップテーブルに保存する必要性は少なくなっていますが、必要であればtransformResponse
を利用して行うことができます。
transformResponse: (response) =>
response.reduce((acc, curr) => {
acc[curr.id] = curr
return acc
}, {})
/*
will convert:
[
{id: 1, name: 'Harry'},
{id: 2, name: 'Ron'},
{id: 3, name: 'Hermione'},
]
to:
{
1: { id: 1, name: "Harry" },
2: { id: 2, name: "Ron" },
3: { id: 3, name: "Hermione" },
}
*/
createEntityAdapter
は、transformResponse
と共に使用してデータを正規化し、ids
配列を提供すること、sortComparer
を使用して一貫してソートされたリストを維持すること、強力なTypeScriptサポートを維持することなど、createEntityAdapter
が提供するその他の機能も利用できます。
こちらも参照してください 変換されたレスポンス形状を持つWebsocketチャットAPI。これは、ストリーミング更新
を使用してさらにデータを更新しながら、createEntityAdapter
と組み合わせてレスポンスデータを正規化するtransformResponse
の例を示しています。
transformErrorResponse
によるクエリレスポンスのカスタマイズ
createApi
の個々のエンドポイントは、transformErrorResponse
プロパティを受け入れます。これにより、クエリまたはミューテーションによって返されたエラーがキャッシュに保存される前に操作できます。
transformErrorResponse
は、対応するエンドポイントに対して失敗したbaseQuery
が返すエラーで呼び出され、transformErrorResponse
の戻り値はそのエンドポイント呼び出しに関連付けられたキャッシュされたエラーとして使用されます。
デフォルトでは、サーバーからのペイロードが直接返されます。
- TypeScript
- JavaScript
function defaultTransformResponse(
baseQueryReturnValue: unknown,
meta: unknown,
arg: unknown
) {
return baseQueryReturnValue
}
function defaultTransformResponse(baseQueryReturnValue, meta, arg) {
return baseQueryReturnValue
}
変更するには、次のような関数を使用します。
transformErrorResponse: (response, meta, arg) =>
response.data.some.deeply.nested.errorObject
transformErrorResponse
は、変換されたレスポンスを決定する際に使用できる、baseQuery
から返されたmeta
プロパティを2番目の引数として呼び出されます。meta
の値は、使用されているbaseQuery
によって異なります。
transformErrorResponse
のmetaの例transformErrorResponse: (
response: { data: { sideA: Tracks; sideB: Tracks } },
meta,
arg,
) => {
if (meta?.coinFlip === 'heads') {
return response.data.sideA
}
return response.data.sideB
}
transformErrorResponse
は、変換されたレスポンスを決定する際に使用できる、エンドポイントに提供されたarg
プロパティを3番目の引数として呼び出されます。arg
の値は、使用されているendpoint
と、クエリ/ミューテーション呼び出しで使用される引数によって異なります。
transformErrorResponse
のargの例transformErrorResponse: (response: Posts, meta, arg) => {
return {
originalArg: arg,
error: response,
}
}
queryFn
によるクエリのカスタマイズ
RTK Queryには、HTTP URL(一般的なREST APIなど)と通信するエンドポイントを簡単に定義できるfetchBaseQuery
が付属しています。GraphQLとの統合もあります。しかし、RTK Queryの中核は、HTTPリクエストだけでなく、*任意の*非同期リクエスト/レスポンスシーケンスの読み込み状態とキャッシュされた値を追跡することです。
RTK Queryは、任意の非同期ロジックを実行して結果を返すエンドポイントの定義をサポートしています。createApi
の個々のエンドポイントは、queryFn
プロパティを受け入れます。これにより、任意のロジックを内部に記述できる独自の非同期関数を記述できます。
これは、単一のエンドポイントに対して特に異なる動作が必要な場合、またはクエリ自体が無関係な場合に役立ちます。具体的には、以下のような場合です。
- 異なるベースURLを使用する一度限りのクエリ
- 自動再試行など、異なるリクエスト処理を使用する一度限りのクエリ
- 異なるエラー処理動作を使用する一度限りのクエリ
- FirebaseやSupabaseなどのサードパーティライブラリSDKを使用してリクエストを行うクエリ
- 典型的なリクエスト/レスポンスではない非同期タスクを実行するクエリ
- 単一のクエリで複数のリクエストを実行する(例)
- 関連するクエリがない場合の無効化動作の活用(例)
- 関連する初期リクエストがない場合のストリーミングアップデートの使用(例)
型シグネチャと利用可能なオプションについては、queryFn APIリファレンス
も参照してください。
queryFn
の実装
queryFn
は、インラインのbaseQuery
と考えることができます。これは、baseQuery
と同じ引数、および提供されたbaseQuery
関数自体(arg
、api
、extraOptions
、およびbaseQuery
)で呼び出されます。baseQuery
と同様に、data
プロパティまたはerror
プロパティを持つオブジェクト、またはそのようなオブジェクトを返すpromiseを返すことが期待されます。
基本的なqueryFn
の例
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import { userAPI, User } from './userAPI'
const api = createApi({
baseQuery: fetchBaseQuery({ url: '/' }),
endpoints: (build) => ({
// normal HTTP endpoint using fetchBaseQuery
getPosts: build.query<PostsResponse, void>({
query: () => ({ url: 'posts' }),
}),
// endpoint with a custom `queryFn` and separate async logic
getUser: build.query<User, string>({
queryFn: async (userId: string) => {
try {
const user = await userApi.getUserById(userId)
// Return the result in an object with a `data` field
return { data: user }
} catch (error) {
// Catch any errors and return them as an object with an `error` field
return { error }
}
},
}),
}),
})
queryFn関数の引数
const queryFn = (
args,
{ signal, dispatch, getState },
extraOptions,
baseQuery,
) => {
// omitted
}
queryFn関数の戻り値
- 成功時の期待される結果形式
return { data: YourData }
- エラー時の期待される結果形式
return { error: YourError }
const queryFn = (
args,
{ signal, dispatch, getState },
extraOptions,
baseQuery,
) => {
if (Math.random() > 0.5) return { error: 'Too high!' }
return { data: 'All good!' }
}
例 - baseQuery
Axios baseQuery
この例では、非常に基本的なaxiosベースのbaseQuery
ユーティリティを実装しています。
- TypeScript
- JavaScript
import { createApi } from '@reduxjs/toolkit/query'
import type { BaseQueryFn } from '@reduxjs/toolkit/query'
import axios from 'axios'
import type { AxiosRequestConfig, AxiosError } from 'axios'
const axiosBaseQuery =
(
{ baseUrl }: { baseUrl: string } = { baseUrl: '' }
): BaseQueryFn<
{
url: string
method?: AxiosRequestConfig['method']
data?: AxiosRequestConfig['data']
params?: AxiosRequestConfig['params']
headers?: AxiosRequestConfig['headers']
},
unknown,
unknown
> =>
async ({ url, method, data, params, headers }) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data,
params,
headers,
})
return { data: result.data }
} catch (axiosError) {
const err = axiosError as AxiosError
return {
error: {
status: err.response?.status,
data: err.response?.data || err.message,
},
}
}
}
const api = createApi({
baseQuery: axiosBaseQuery({
baseUrl: 'https://example.com',
}),
endpoints(build) {
return {
query: build.query({ query: () => ({ url: '/query', method: 'get' }) }),
mutation: build.mutation({
query: () => ({ url: '/mutation', method: 'post' }),
}),
}
},
})
import { createApi } from '@reduxjs/toolkit/query'
import axios from 'axios'
const axiosBaseQuery =
({ baseUrl } = { baseUrl: '' }) =>
async ({ url, method, data, params, headers }) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data,
params,
headers,
})
return { data: result.data }
} catch (axiosError) {
const err = axiosError
return {
error: {
status: err.response?.status,
data: err.response?.data || err.message,
},
}
}
}
const api = createApi({
baseQuery: axiosBaseQuery({
baseUrl: 'https://example.com',
}),
endpoints(build) {
return {
query: build.query({ query: () => ({ url: '/query', method: 'get' }) }),
mutation: build.mutation({
query: () => ({ url: '/mutation', method: 'post' }),
}),
}
},
})
GraphQL baseQuery
この例では、非常に基本的なGraphQLベースのbaseQuery
を実装しています。
- TypeScript
- JavaScript
import { createApi } from '@reduxjs/toolkit/query'
import { request, gql, ClientError } from 'graphql-request'
const graphqlBaseQuery =
({ baseUrl }: { baseUrl: string }) =>
async ({ body }: { body: string }) => {
try {
const result = await request(baseUrl, body)
return { data: result }
} catch (error) {
if (error instanceof ClientError) {
return { error: { status: error.response.status, data: error } }
}
return { error: { status: 500, data: error } }
}
}
export const api = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: 'https://graphqlzero.almansi.me/api',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => ({
body: gql`
query {
posts {
data {
id
title
}
}
}
`,
}),
transformResponse: (response) => response.posts.data,
}),
getPost: builder.query({
query: (id) => ({
body: gql`
query {
post(id: ${id}) {
id
title
body
}
}
`,
}),
transformResponse: (response) => response.post,
}),
}),
})
import { createApi } from '@reduxjs/toolkit/query'
import { request, gql, ClientError } from 'graphql-request'
const graphqlBaseQuery =
({ baseUrl }) =>
async ({ body }) => {
try {
const result = await request(baseUrl, body)
return { data: result }
} catch (error) {
if (error instanceof ClientError) {
return { error: { status: error.response.status, data: error } }
}
return { error: { status: 500, data: error } }
}
}
export const api = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: 'https://graphqlzero.almansi.me/api',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => ({
body: gql`
query {
posts {
data {
id
title
}
}
}
`,
}),
transformResponse: (response) => response.posts.data,
}),
getPost: builder.query({
query: (id) => ({
body: gql`
query {
post(id: ${id}) {
id
title
body
}
}
`,
}),
transformResponse: (response) => response.post,
}),
}),
})
fetchBaseQueryを拡張した自動再認証
この例では、fetchBaseQuery
をラップして、401 Unauthorized
エラーが発生した場合、認証トークンを更新しようとする追加のリクエストを送信し、再認証後に初期クエリを再試行します。
- TypeScript
- JavaScript
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import type {
BaseQueryFn,
FetchArgs,
FetchBaseQueryError,
} from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'
const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
// try to get a new token
const refreshResult = await baseQuery('/refreshToken', api, extraOptions)
if (refreshResult.data) {
// store the new token
api.dispatch(tokenReceived(refreshResult.data))
// retry the initial query
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
}
return result
}
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'
const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
// try to get a new token
const refreshResult = await baseQuery('/refreshToken', api, extraOptions)
if (refreshResult.data) {
// store the new token
api.dispatch(tokenReceived(refreshResult.data))
// retry the initial query
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
}
return result
}
複数の未承認エラーの防止
async-mutex
を使用して、401 Unauthorized
エラーで複数回呼び出しが失敗した場合の'/refreshToken'への複数回の呼び出しを防ぎます。
- TypeScript
- JavaScript
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import type {
BaseQueryFn,
FetchArgs,
FetchBaseQueryError,
} from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'
import { Mutex } from 'async-mutex'
// create a new mutex
const mutex = new Mutex()
const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
// wait until the mutex is available without locking it
await mutex.waitForUnlock()
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
// checking whether the mutex is locked
if (!mutex.isLocked()) {
const release = await mutex.acquire()
try {
const refreshResult = await baseQuery(
'/refreshToken',
api,
extraOptions
)
if (refreshResult.data) {
api.dispatch(tokenReceived(refreshResult.data))
// retry the initial query
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
} finally {
// release must be called once the mutex should be released again.
release()
}
} else {
// wait until the mutex is available without locking it
await mutex.waitForUnlock()
result = await baseQuery(args, api, extraOptions)
}
}
return result
}
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'
import { Mutex } from 'async-mutex'
// create a new mutex
const mutex = new Mutex()
const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth = async (args, api, extraOptions) => {
// wait until the mutex is available without locking it
await mutex.waitForUnlock()
let result = await baseQuery(args, api, extraOptions)
if (result.error && result.error.status === 401) {
// checking whether the mutex is locked
if (!mutex.isLocked()) {
const release = await mutex.acquire()
try {
const refreshResult = await baseQuery(
'/refreshToken',
api,
extraOptions
)
if (refreshResult.data) {
api.dispatch(tokenReceived(refreshResult.data))
// retry the initial query
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
} finally {
// release must be called once the mutex should be released again.
release()
}
} else {
// wait until the mutex is available without locking it
await mutex.waitForUnlock()
result = await baseQuery(args, api, extraOptions)
}
}
return result
}
自動再試行
RTK Queryは、API定義でbaseQuery
をラップできるretry
というユーティリティをエクスポートします。これは、基本的な指数バックオフを使用して、デフォルトで5回の試行を行います。
デフォルトの動作では、これらの間隔で再試行されます。
- 600ms * random(0.4, 1.4)
- 1200ms * random(0.4, 1.4)
- 2400ms * random(0.4, 1.4)
- 4800ms * random(0.4, 1.4)
- 9600ms * random(0.4, 1.4)
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
interface Post {
id: number
name: string
}
type PostsResponse = Post[]
// maxRetries: 5 is the default, and can be omitted. Shown for documentation purposes.
const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), {
maxRetries: 5,
})
export const api = createApi({
baseQuery: staggeredBaseQuery,
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => ({ url: 'posts' }),
}),
getPost: build.query<PostsResponse, string>({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
// maxRetries: 5 is the default, and can be omitted. Shown for documentation purposes.
const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), {
maxRetries: 5,
})
export const api = createApi({
baseQuery: staggeredBaseQuery,
endpoints: (build) => ({
getPosts: build.query({
query: () => ({ url: 'posts' }),
}),
getPost: build.query({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
特定のエンドポイントで再試行したくない場合は、maxRetries: 0
を設定するだけです。
フックが同時にdata
とerror
を返すことが可能です。デフォルトでは、RTK Queryは、更新またはガベージコレクションされるまで、最後の「良好な」結果をdata
に保持します。
エラー再試行の中断
retry
ユーティリティには、再試行をすぐに中断するために使用できるfail
メソッドプロパティが添付されています。これは、追加の再試行がすべて失敗することが保証され、冗長になることがわかっている状況で使用できます。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
import type { FetchArgs } from '@reduxjs/toolkit/query'
interface Post {
id: number
name: string
}
type PostsResponse = Post[]
const staggeredBaseQueryWithBailOut = retry(
async (args: string | FetchArgs, api, extraOptions) => {
const result = await fetchBaseQuery({ baseUrl: '/api/' })(
args,
api,
extraOptions
)
// bail out of re-tries immediately if unauthorized,
// because we know successive re-retries would be redundant
if (result.error?.status === 401) {
retry.fail(result.error)
}
return result
},
{
maxRetries: 5,
}
)
export const api = createApi({
baseQuery: staggeredBaseQueryWithBailOut,
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => ({ url: 'posts' }),
}),
getPost: build.query<Post, string>({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
const staggeredBaseQueryWithBailOut = retry(
async (args, api, extraOptions) => {
const result = await fetchBaseQuery({ baseUrl: '/api/' })(
args,
api,
extraOptions
)
// bail out of re-tries immediately if unauthorized,
// because we know successive re-retries would be redundant
if (result.error?.status === 401) {
retry.fail(result.error)
}
return result
},
{
maxRetries: 5,
}
)
export const api = createApi({
baseQuery: staggeredBaseQueryWithBailOut,
endpoints: (build) => ({
getPosts: build.query({
query: () => ({ url: 'posts' }),
}),
getPost: build.query({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
クエリへのメタ情報の追加
baseQuery
は、戻り値にmeta
プロパティを含めることもできます。これは、リクエストIDやタイムスタンプなど、リクエストに関連する追加情報を追加したい場合に役立ちます。
そのようなシナリオでは、戻り値は次のようになります。
- メタ情報を含む成功結果の期待される形式
return { data: YourData, meta: YourMeta }
- メタ情報を含むエラー結果の期待される形式
return { error: YourError, meta: YourMeta }
- TypeScript
- JavaScript
import { fetchBaseQuery, createApi } from '@reduxjs/toolkit/query'
import type {
BaseQueryFn,
FetchArgs,
FetchBaseQueryError,
} from '@reduxjs/toolkit/query'
import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/dist/query/fetchBaseQuery'
import { uuid } from './idGenerator'
type Meta = {
requestId: string
timestamp: number
}
const metaBaseQuery: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError,
{},
Meta & FetchBaseQueryMeta
> = async (args, api, extraOptions) => {
const requestId = uuid()
const timestamp = Date.now()
const baseResult = await fetchBaseQuery({ baseUrl: '/' })(
args,
api,
extraOptions
)
return {
...baseResult,
meta: baseResult.meta && { ...baseResult.meta, requestId, timestamp },
}
}
const DAY_MS = 24 * 60 * 60 * 1000
interface Post {
id: number
name: string
timestamp: number
}
type PostsResponse = Post[]
const api = createApi({
baseQuery: metaBaseQuery,
endpoints: (build) => ({
// a theoretical endpoint where we only want to return data
// if request was performed past a certain date
getRecentPosts: build.query<PostsResponse, void>({
query: () => 'posts',
transformResponse: (returnValue: PostsResponse, meta) => {
// `meta` here contains our added `requestId` & `timestamp`, as well as
// `request` & `response` from fetchBaseQuery's meta object.
// These properties can be used to transform the response as desired.
if (!meta) return []
return returnValue.filter(
(post) => post.timestamp >= meta.timestamp - DAY_MS
)
},
}),
}),
})
import { fetchBaseQuery, createApi } from '@reduxjs/toolkit/query'
import { uuid } from './idGenerator'
const metaBaseQuery = async (args, api, extraOptions) => {
const requestId = uuid()
const timestamp = Date.now()
const baseResult = await fetchBaseQuery({ baseUrl: '/' })(
args,
api,
extraOptions
)
return {
...baseResult,
meta: baseResult.meta && { ...baseResult.meta, requestId, timestamp },
}
}
const DAY_MS = 24 * 60 * 60 * 1000
const api = createApi({
baseQuery: metaBaseQuery,
endpoints: (build) => ({
// a theoretical endpoint where we only want to return data
// if request was performed past a certain date
getRecentPosts: build.query({
query: () => 'posts',
transformResponse: (returnValue, meta) => {
// `meta` here contains our added `requestId` & `timestamp`, as well as
// `request` & `response` from fetchBaseQuery's meta object.
// These properties can be used to transform the response as desired.
if (!meta) return []
return returnValue.filter(
(post) => post.timestamp >= meta.timestamp - DAY_MS
)
},
}),
}),
})
Redux状態を使用した動的な基本URLの構築
場合によっては、Redux状態のプロパティから決定される動的に変更される基本URLが必要になる場合があります。baseQuery
は、呼び出された時点の現在のストアの状態を提供するgetState
メソッドにアクセスできます。これは、部分的なURL文字列とストア状態の適切なデータを使用して、目的のURLを構築するために使用できます。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type {
BaseQueryFn,
FetchArgs,
FetchBaseQueryError,
} from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
import { selectProjectId } from './projectSlice'
import type { RootState } from '../store'
const rawBaseQuery = fetchBaseQuery({
baseUrl: 'www.my-cool-site.com/',
})
const dynamicBaseQuery: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
const projectId = selectProjectId(api.getState() as RootState)
// gracefully handle scenarios where data to generate the URL is missing
if (!projectId) {
return {
error: {
status: 400,
statusText: 'Bad Request',
data: 'No project ID received',
},
}
}
const urlEnd = typeof args === 'string' ? args : args.url
// construct a dynamically generated portion of the url
const adjustedUrl = `project/${projectId}/${urlEnd}`
const adjustedArgs =
typeof args === 'string' ? adjustedUrl : { ...args, url: adjustedUrl }
// provide the amended url and other params to the raw base query
return rawBaseQuery(adjustedArgs, api, extraOptions)
}
export const api = createApi({
baseQuery: dynamicBaseQuery,
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => 'posts',
}),
}),
})
export const { useGetPostsQuery } = api
/*
Using `useGetPostsQuery()` where a `projectId` of 500 is in the redux state will result in
a request being sent to www.my-cool-site.com/project/500/posts
*/
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { selectProjectId } from './projectSlice'
const rawBaseQuery = fetchBaseQuery({
baseUrl: 'www.my-cool-site.com/',
})
const dynamicBaseQuery = async (args, api, extraOptions) => {
const projectId = selectProjectId(api.getState())
// gracefully handle scenarios where data to generate the URL is missing
if (!projectId) {
return {
error: {
status: 400,
statusText: 'Bad Request',
data: 'No project ID received',
},
}
}
const urlEnd = typeof args === 'string' ? args : args.url
// construct a dynamically generated portion of the url
const adjustedUrl = `project/${projectId}/${urlEnd}`
const adjustedArgs =
typeof args === 'string' ? adjustedUrl : { ...args, url: adjustedUrl }
// provide the amended url and other params to the raw base query
return rawBaseQuery(adjustedArgs, api, extraOptions)
}
export const api = createApi({
baseQuery: dynamicBaseQuery,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
}),
}),
})
export const { useGetPostsQuery } = api
/*
Using `useGetPostsQuery()` where a `projectId` of 500 is in the redux state will result in
a request being sent to www.my-cool-site.com/project/500/posts
*/
例 - transformResponse
深くネストされたGraphQLデータの展開
- TypeScript
- JavaScript
import { createApi } from '@reduxjs/toolkit/query'
import { graphqlBaseQuery, gql } from './graphqlBaseQuery'
interface Post {
id: number
title: string
}
export const api = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: '/graphql',
}),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => ({
body: gql`
query {
posts {
data {
id
title
}
}
}
`,
}),
transformResponse: (response: { posts: { data: Post[] } }) =>
response.posts.data,
}),
}),
})
import { createApi } from '@reduxjs/toolkit/query'
import { graphqlBaseQuery, gql } from './graphqlBaseQuery'
export const api = createApi({
baseQuery: graphqlBaseQuery({
baseUrl: '/graphql',
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => ({
body: gql`
query {
posts {
data {
id
title
}
}
}
`,
}),
transformResponse: (response) => response.posts.data,
}),
}),
})
createEntityAdapter
を使用したデータの正規化
以下の例では、transformResponse
をcreateEntityAdapter
と組み合わせて使用して、キャッシュに保存する前にデータを正規化します。
次のようなレスポンスの場合
[
{ id: 1, name: 'Harry' },
{ id: 2, name: 'Ron' },
{ id: 3, name: 'Hermione' },
]
正規化されたキャッシュデータは次のように保存されます。
{
ids: [1, 3, 2],
entities: {
1: { id: 1, name: "Harry" },
2: { id: 2, name: "Ron" },
3: { id: 3, name: "Hermione" },
}
}
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { createEntityAdapter } from '@reduxjs/toolkit'
import type { EntityState } from '@reduxjs/toolkit'
export interface Post {
id: number
name: string
}
const postsAdapter = createEntityAdapter<Post>({
sortComparer: (a, b) => a.name.localeCompare(b.name),
})
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPosts: build.query<EntityState<Post, number>, void>({
query: () => `posts`,
transformResponse(response: Post[]) {
return postsAdapter.addMany(postsAdapter.getInitialState(), response)
},
}),
}),
})
export const { useGetPostsQuery } = api
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { createEntityAdapter } from '@reduxjs/toolkit'
const postsAdapter = createEntityAdapter({
sortComparer: (a, b) => a.name.localeCompare(b.name),
})
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPosts: build.query({
query: () => `posts`,
transformResponse(response) {
return postsAdapter.addMany(postsAdapter.getInitialState(), response)
},
}),
}),
})
export const { useGetPostsQuery } = api
例 - queryFn
サードパーティSDKの使用
FirebaseやSupabaseなどの多くのサービスは、リクエストを行うための独自のSDKを提供しています。これらのSDKメソッドをqueryFn
で使用できます。
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react'
import { supabase } from './supabaseApi'
export const supabaseApi = createApi({
reducerPath: 'supabaseApi',
baseQuery: fakeBaseQuery(),
endpoints: (builder) => ({
getBlogs: builder.query({
queryFn: async () => {
// Supabase conveniently already has `data` and `error` fields
const { data, error } = await supabase.from('blogs').select()
if (error) {
return { error }
}
return { data }
},
}),
}),
})
SDKを使用するカスタムベースクエリを作成し、メソッド名または引数をそのベースクエリに渡すエンドポイントを定義することもできます。
no-op queryFnの使用
特定のシナリオでは、リクエストの送信やデータの返却が状況に関係ないquery
またはmutation
が必要になる場合があります。このようなシナリオでは、invalidatesTags
プロパティを利用して、キャッシュに提供されている特定のtags
の再取得を強制します。
このようなシナリオで「エラーが発生したクエリの再取得」の詳細と例については、キャッシュへのエラーの提供
も参照してください。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post, User } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Post', 'User'],
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
query: () => 'posts',
providesTags: ['Post'],
}),
getUsers: build.query<User[], void>({
query: () => 'users',
providesTags: ['User'],
}),
refetchPostsAndUsers: build.mutation<null, void>({
// The query is not relevant here, so a `null` returning `queryFn` is used
queryFn: () => ({ data: null }),
// This mutation takes advantage of tag invalidation behaviour to trigger
// any queries that provide the 'Post' or 'User' tags to re-fetch if the queries
// are currently subscribed to the cached data
invalidatesTags: ['Post', 'User'],
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Post', 'User'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
providesTags: ['Post'],
}),
getUsers: build.query({
query: () => 'users',
providesTags: ['User'],
}),
refetchPostsAndUsers: build.mutation({
// The query is not relevant here, so a `null` returning `queryFn` is used
queryFn: () => ({ data: null }),
// This mutation takes advantage of tag invalidation behaviour to trigger
// any queries that provide the 'Post' or 'User' tags to re-fetch if the queries
// are currently subscribed to the cached data
invalidatesTags: ['Post', 'User'],
}),
}),
})
初期リクエストのないストリーミングデータ
RTK Queryでは、エンドポイントがデータの初期リクエストを送信し、その後、更新が発生するとキャッシュされたデータにさらに更新を実行する定期的なストリーミングアップデートを行うことができます。ただし、初期リクエストはオプションであり、初期リクエストを送信せずにストリーミングアップデートを使用することもできます。
以下の例では、queryFn
を使用して、初期リクエストを送信せずに空の配列でキャッシュデータを設定します。配列は後で、onCacheEntryAdded
エンドポイントオプションを介してストリーミングアップデートを使用して設定され、受信されたデータがキャッシュデータに更新されます。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Message } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Message'],
endpoints: (build) => ({
streamMessages: build.query<Message[], void>({
// The query is not relevant here as the data will be provided via streaming updates.
// A queryFn returning an empty array is used, with contents being populated via
// streaming updates below as they are received.
queryFn: () => ({ data: [] }),
async onCacheEntryAdded(arg, { updateCachedData, cacheEntryRemoved }) {
const ws = new WebSocket('ws://localhost:8080')
// populate the array with messages as they are received from the websocket
ws.addEventListener('message', (event) => {
updateCachedData((draft) => {
draft.push(JSON.parse(event.data))
})
})
await cacheEntryRemoved
ws.close()
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Message'],
endpoints: (build) => ({
streamMessages: build.query({
// The query is not relevant here as the data will be provided via streaming updates.
// A queryFn returning an empty array is used, with contents being populated via
// streaming updates below as they are received.
queryFn: () => ({ data: [] }),
async onCacheEntryAdded(arg, { updateCachedData, cacheEntryRemoved }) {
const ws = new WebSocket('ws://localhost:8080')
// populate the array with messages as they are received from the websocket
ws.addEventListener('message', (event) => {
updateCachedData((draft) => {
draft.push(JSON.parse(event.data))
})
})
await cacheEntryRemoved
ws.close()
},
}),
}),
})
単一のクエリで複数のリクエストを実行する
以下の例では、ランダムなユーザーのすべての投稿を取得するクエリを作成します。これは、ランダムなユーザーに対する最初のリクエストを行い、そのユーザーのすべての投稿を取得することによって行われます。queryFn
を使用すると、2つのリクエストを単一のクエリに含めることができ、コンポーネントコード内でそのロジックをチェーンする必要がなくなります。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
import type { Post, User } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/ ' }),
endpoints: (build) => ({
getRandomUserPosts: build.query<Post, void>({
async queryFn(_arg, _queryApi, _extraOptions, fetchWithBQ) {
// get a random user
const randomResult = await fetchWithBQ('users/random')
if (randomResult.error)
return { error: randomResult.error as FetchBaseQueryError }
const user = randomResult.data as User
const result = await fetchWithBQ(`user/${user.id}/posts`)
return result.data
? { data: result.data as Post }
: { error: result.error as FetchBaseQueryError }
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/ ' }),
endpoints: (build) => ({
getRandomUserPosts: build.query({
async queryFn(_arg, _queryApi, _extraOptions, fetchWithBQ) {
// get a random user
const randomResult = await fetchWithBQ('users/random')
if (randomResult.error) return { error: randomResult.error }
const user = randomResult.data
const result = await fetchWithBQ(`user/${user.id}/posts`)
return result.data ? { data: result.data } : { error: result.error }
},
}),
}),
})