Customizing queries#

RTK Query is agnostic as to how your requests resolve. You can use any library you like to handle requests, or no library at all. By default, RTK Query ships with fetchBaseQuery, which is a lightweight fetch wrapper that automatically handles request headers and response parsing in a manner similar to common libraries like axios. If fetchBaseQuery alone does not meet your needs, you can customize it's behaviour with a wrapper function, or create your own baseQuery function from scratch for createApi to use.

Implementing a custom baseQuery#

RTK Query expects a baseQuery function to be called with three arguments: args, api, and extraOptions. It is expected to return an object with either a data or error property, or a promise that resolves to return such an object.

baseQuery function arguments#

  • args - The return value of the query function for a given endpoint
  • api - The BaseQueryApi object, containing signal, dispatch and getState properties
    • signal - An AbortSignal object that may be used to abort DOM requests and/or read whether the request is aborted.
    • dispatch - The store.dispatch method for the corresponding Redux store
    • getState - A function that may be called to access the current store state
  • extraOptions - The value of the optional extraOptions property provided for a given endpoint
baseQuery example arguments
const customBaseQuery = (
args,
{ signal, dispatch, getState },
extraOptions
) => {
// omitted
}

baseQuery function return value#

  1. Expected success result format
    return { data: YourData }
  2. Expected error result format
    return { error: YourError }
baseQuery example return value
const customBaseQuery = (
args,
{ signal, dispatch, getState },
extraOptions
) => {
if (Math.random() > 0.5) return { error: 'Too high!' }
return { data: 'All good!' }
}

The type for data is dictated based on the types specified per endpoint (both queries & mutations), while the type for error is dictated by the baseQuery function used.

note

This format is required so that RTK Query can infer the return types for your responses.

baseQuery function signature#

The signature of a baseQuery function is as follows:

Base Query signature
export type BaseQueryFn<
Args = any,
Result = unknown,
Error = unknown,
DefinitionExtraOptions = {},
Meta = {}
> = (
args: Args,
api: BaseQueryApi,
extraOptions: DefinitionExtraOptions
) => MaybePromise<QueryReturnValue<Result, Error, Meta>>
export interface BaseQueryApi {
signal: AbortSignal
dispatch: ThunkDispatch<any, any, any>
getState: () => unknown
}
export type QueryReturnValue<T = unknown, E = unknown, M = unknown> =
| {
error: E
data?: undefined
meta?: M
}
| {
error?: undefined
data: T
meta?: M
}

A custom baseQuery need only follow the BaseQueryFn signature above in order to be used. The examples further on demonstrate multiple implementations of baseQuery functions to meet different requirements.

fetchBaseQuery defaults#

For fetchBaseQuery specifically, the return type is as follows:

Return types of fetchBaseQuery
Promise<{
data: any;
error?: undefined;
meta?: { request: Request; response: Response };
} | {
error: {
status: number;
data: any;
};
data?: undefined;
meta?: { request: Request; response: Response };
}>
  1. Expected success result format with fetchBaseQuery
    return { data: YourData }
  2. Expected error result format with fetchBaseQuery
    return { error: { status: number, data: YourErrorData } }

Examples#

Axios Base Query#

This example implements a very basic axios-based baseQuery utility.

Basic axios baseQuery
import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query'
import axios, { AxiosRequestConfig, AxiosError } from 'axios'
const axiosBaseQuery = (
{ baseUrl }: { baseUrl: string } = { baseUrl: '' }
): BaseQueryFn<
{
url: string
method: AxiosRequestConfig['method']
data?: AxiosRequestConfig['data']
},
unknown,
unknown
> => async ({ url, method, data }) => {
try {
const result = await axios({ url: baseUrl + url, method, data })
return { data: result.data }
} catch (axiosError) {
let err = axiosError as AxiosError
return { error: { status: err.response?.status, data: err.response?.data } }
}
}
const api = createApi({
baseQuery: axiosBaseQuery({
baseUrl: 'http://example.com',
}),
endpoints(build) {
return {
query: build.query({ query: () => ({ url: '/query', method: 'get' }) }),
mutation: build.mutation({
query: () => ({ url: '/mutation', method: 'post' }),
}),
}
},
})

Automatic re-authorization by extending fetchBaseQuery#

This example wraps fetchBaseQuery such that when encountering a 401 Unauthorized error, an additional request is sent to attempt to refresh an authorization token, and re-try to initial query after re-authorizing.

Simulating axios-like interceptors with a custom base query
import {
BaseQueryFn,
FetchArgs,
fetchBaseQuery,
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
}

Automatic retries#

RTK Query exports a utility called retry that you can wrap the baseQuery in your API definition with. It defaults to 5 attempts with a basic exponential backoff.

The default behavior would retry at these intervals:

  1. 600ms * random(0.4, 1.4)
  2. 1200ms * random(0.4, 1.4)
  3. 2400ms * random(0.4, 1.4)
  4. 4800ms * random(0.4, 1.4)
  5. 9600ms * random(0.4, 1.4)
Retry every request 5 times by default
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: `posts/${id}` }),
extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api

In the event that you didn't want to retry on a specific endpoint, you can just set maxRetries: 0.

info

It is possible for a hook to return data and error at the same time. By default, RTK Query will keep whatever the last 'good' result was in data until it can be updated or garbage collected.

Bailing out of error re-tries#

The retry utility has a fail method property attached which can be used to bail out of retries immediately. This can be used for situations where it is known that additional re-tries would be guaranteed to all fail and would be redundant.

Bailing out of error re-tries
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
import { FetchArgs } from '@reduxjs/toolkit/dist/query/fetchBaseQuery'
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<PostsResponse, string>({
query: (id) => ({ url: `posts/${id}` }),
extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api

Excluding baseQuery with queryFn#

Individual endpoints on createApi accept a queryFn property which allows a given endpoint to ignore baseQuery for that endpoint by providing an inline function determining how that query resolves.

This can be useful for scenarios where you want to have particularly different behaviour for a single endpoint, or where the query itself is not relevant. Such situations may include:

  • One-off queries that use a different base URL
  • One-off queries that use different handling, such as automatic re-tries
  • One off queries that use different error handling behaviour
  • Leveraging invalidation behaviour with no relevant query
  • Using Streaming Updates with no relevant initial query
Excluding baseQuery for a single endpoint
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import { Post, User, Message } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Post', 'User', 'Message'],
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'],
}),
streamMessages: build.query<Message[], void>({
// The query it 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()
},
}),
}),
})

For typescript users, the error type that queryFn must return is dictated by the provided baseQuery function. For users who wish to only use queryFn for each endpoint and not include a baseQuery at all, RTK Query provides a fakeBaseQuery function that can be used to easily specify the error type each queryFn should return.

Excluding baseQuery for all endpoints
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'
type CustomErrorType = { type: 'too cold' | 'too hot' }
const api = createApi({
baseQuery: fakeBaseQuery<CustomErrorType>(),
endpoints: (build) => ({
eatPorridge: build.query<'just right', 1 | 2 | 3>({
queryFn(seat) {
if (seat === 1) {
return { error: { type: 'too cold' } }
}
if (seat === 2) {
return { error: { type: 'too hot' } }
}
return { data: 'just right' }
},
}),
microwaveHotPocket: build.query<'delicious!', number>({
queryFn(duration) {
if (duration < 110) {
return { error: { type: 'too cold' } }
}
if (duration > 140) {
return { error: { type: 'too hot' } }
}
return { data: 'delicious!' }
},
}),
}),
})

Adding Meta information to queries#

A baseQuery can also include a meta property in it's return value. This can be beneficial in cases where you may wish to include additional information associated with the request such as a request ID or timestamp.

In such a scenario, the return value would look like so:

  1. Expected success result format with meta
    return { data: YourData, meta: YourMeta }
  2. Expected error result format with meta
    return { error: YourError, meta: YourMeta }
baseQuery example with meta information
import {
BaseQueryFn,
FetchArgs,
fetchBaseQuery,
FetchBaseQueryError,
createApi,
} from '@reduxjs/toolkit/query'
import { 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
)
},
}),
}),
})