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. RTK Query provides reasonable defaults expected to cover the majority of use cases, while also allowing room for customization to alter query handling to fit specific needs.
Customizing queries with baseQuery
The default method to handle queries is via the baseQuery option on createApi, in combination with the query option on an endpoint definition.
To process queries, endpoints are defined with a query option, which passes its return value to a common baseQuery function used for the API.
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 its behaviour with a wrapper function, or create your own baseQuery function from scratch for createApi to use.
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.
:::note This format is required so that RTK Query can infer the return types for your responses. :::
At its core, a baseQuery function only needs to have the minimum return value to be valid; an object with a data or error property. It is up to the user to determine how they wish to use the provided arguments, and how requests are handled within the function itself.
fetchBaseQuery defaults
For fetchBaseQuery specifically, the return type is as follows:
Customizing query responses with transformResponse
Individual endpoints on createApi accept a transformResponse property which allows manipulation of the data returned by a query or mutation before it hits the cache.
transformResponse is called with the data that a successful baseQuery returns for the corresponding endpoint, and the return value of transformResponse is used as the cached data associated with that endpoint call.
By default, the payload from the server is returned directly.
transformResponse is also called with the meta property returned from the baseQuery, which can be used while determining the transformed response. The value for meta is dependent on the baseQuery used.
While there is less need to store the response in a normalized lookup table with RTK Query managing caching data, transformResponse can be leveraged to do so if desired.
createEntityAdapter can also be used with transformResponse to normalize data, while also taking advantage of other features provided by createEntityAdapter, including providing an ids array, using sortComparer to maintain a consistently sorted list, as well as maintaining strong TypeScript support.
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 request handling, such as automatic re-tries
One-off queries that use different error handling behaviour
Performing multiple requests with a single query (example)
Leveraging invalidation behaviour with no relevant query (example)
In order to use queryFn, it can be treated as an inline baseQuery. It will be called with the same arguments as baseQuery, as well as the provided baseQuery function itself (arg, api, extraOptions, and baseQuery). Similarly to baseQuery, it is expected to return an object with either a data or error property, or a promise that resolves to return such an object.
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.
// file: authSlice.ts noEmitdeclarefunctiontokenReceived(args?:any):voiddeclarefunctionloggedOut():voidexport { tokenReceived, loggedOut }// file: baseQueryWithReauth.tsimport { BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError,} from'@reduxjs/toolkit/query'import { tokenReceived, loggedOut } from'./authSlice'constbaseQuery=fetchBaseQuery({ baseUrl:'/' })constbaseQueryWithReauth:BaseQueryFn<string|FetchArgs,unknown,FetchBaseQueryError> =async (args, api, extraOptions) => {let result =awaitbaseQuery(args, api, extraOptions)if (result.error &&result.error.status ===401) {// try to get a new tokenconstrefreshResult=awaitbaseQuery('/refreshToken', api, extraOptions)if (refreshResult.data) {// store the new tokenapi.dispatch(tokenReceived(refreshResult.data))// retry the initial query result =awaitbaseQuery(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:
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.
import { createApi, fetchBaseQuery, retry } from'@reduxjs/toolkit/query/react'import { FetchArgs } from'@reduxjs/toolkit/dist/query/fetchBaseQuery'interfacePost { id:number name:string}typePostsResponse=Post[]// highlight-startconststaggeredBaseQueryWithBailOut=retry(async (args:string|FetchArgs, api, extraOptions) => {constresult=awaitfetchBaseQuery({ baseUrl:'/api/' })( args, api, extraOptions )// bail out of re-tries immediately if unauthorized,// because we know successive re-retries would be redundantif (result.error?.status ===401) {retry.fail(result.error) }return result }, { maxRetries:5, })// highlight-endexportconstapi=createApi({// highlight-start baseQuery: staggeredBaseQueryWithBailOut,// highlight-endendpoints: (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 }), }),})exportconst { useGetPostsQuery,useGetPostQuery } = api
Adding Meta information to queries
A baseQuery can also include a meta property in its 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:
return { data: YourData, meta: YourMeta }
return { error: YourError, meta: YourMeta }
// file: idGenerator.ts noEmitexportdeclareconstuuid: () =>string// file: metaBaseQuery.tsimport { BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError, createApi,} from'@reduxjs/toolkit/query'import { FetchBaseQueryMeta } from'@reduxjs/toolkit/dist/query/fetchBaseQuery'import { uuid } from'./idGenerator'// highlight-starttypeMeta= { requestId:string timestamp:number}// highlight-end// highlight-startconstmetaBaseQuery:BaseQueryFn<string|FetchArgs,unknown,FetchBaseQueryError, {},Meta&FetchBaseQueryMeta> =async (args, api, extraOptions) => {constrequestId=uuid()consttimestamp=Date.now()constbaseResult=awaitfetchBaseQuery({ baseUrl:'/' })( args, api, extraOptions )return {...baseResult, meta:baseResult.meta && { ...baseResult.meta, requestId, timestamp }, }}// highlight-endconstDAY_MS=24*60*60*1000interfacePost { id:number name:string timestamp:number}typePostsResponse=Post[]constapi=createApi({// highlight-start baseQuery: metaBaseQuery,// highlight-endendpoints: (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',// highlight-starttransformResponse: (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 []returnreturnValue.filter( (post) =>post.timestamp >=meta.timestamp -DAY_MS ) },// highlight-end }), }),})
Constructing a Dynamic Base URL using Redux state
In some cases, you may wish to have a dynamically altered base url determined from a property in your Redux state. A baseQuery has access to a getState method that provides the current store state at the time it is called. This can be used to construct the desired url using a partial url string, and the appropriate data from your store state.
// file: src/store.ts noEmitexporttypeRootState= { auth: { projectId:number|null }}// file: src/services/projectSlice.ts noEmitimporttype { RootState } from'../store'exportconstselectProjectId= (state:RootState) =>state.auth.projectId// file: src/services/types.ts noEmitexportinterfacePost { id:number name:string}// file: src/services/api.tsimport { createApi, BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError,} from'@reduxjs/toolkit/query/react'importtype { Post } from'./types'import { selectProjectId } from'./projectSlice'importtype { RootState } from'../store'constrawBaseQuery=fetchBaseQuery({ baseUrl:'www.my-cool-site.com/',})constdynamicBaseQuery:BaseQueryFn<string|FetchArgs,unknown,FetchBaseQueryError> =async (args, api, extraOptions) => {constprojectId=selectProjectId(api.getState() asRootState)// gracefully handle scenarios where data to generate the URL is missingif (!projectId) {return { error: { status:400, data:'No project ID received', }, } }consturlEnd=typeof args ==='string'? args :args.url// construct a dynamically generated portion of the urlconstadjustedUrl=`project/${projectId}/${urlEnd}`constadjustedArgs=typeof args ==='string'? adjustedUrl : { ...args, url: adjustedUrl }// provide the amended url and other params to the raw base queryreturnrawBaseQuery(adjustedArgs, api, extraOptions)}exportconstapi=createApi({ baseQuery: dynamicBaseQuery,endpoints: (builder) => ({ getPosts:builder.query<Post[],void>({query: () =>'posts', }), }),})exportconst { 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*/
In certain scenarios, you may wish to have a query or mutation where sending a request or returning data is not relevant for the situation. Such a scenario would be to leverage the invalidatesTags property to force re-fetch specific tags that have been provided to the cache.
See also providing errors to the cache to see additional detail and an example for such a scenario to 'refetch errored queries'.
// file: types.ts noEmitexportinterfacePost { id:number name:string}exportinterfaceUser { id:number name:string}// file: api.tsimport { createApi, fetchBaseQuery } from'@reduxjs/toolkit/query'import { Post, User } from'./types'constapi=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'], }),// highlight-start refetchPostsAndUsers:build.mutation<null,void>({// The query is not relevant here, so a `null` returning `queryFn` is usedqueryFn: () => ({ 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'], }),// highlight-end }),})
Streaming data with no initial request
RTK Query provides the ability for an endpoint to send an initial request for data, followed up with recurring streaming updates that perform further updates to the cached data as the updates occur. However, the initial request is optional, and you may wish to use streaming updates without any initial request fired off.
In the example below, a queryFn is used to populate the cache data with an empty array, with no initial request sent. The array is later populated using streaming updates via the onCacheEntryAdded endpoint option, updating the cached data as it is received.
// file: types.ts noEmitexportinterfaceMessage { id:number channel:'general'|'redux' userName:string text:string}// file: api.tsimport { createApi, fetchBaseQuery } from'@reduxjs/toolkit/query'import { Message } from'./types'constapi=createApi({ baseQuery:fetchBaseQuery({ baseUrl:'/' }), tagTypes: ['Message'],endpoints: (build) => ({// highlight-start 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: [] }),asynconCacheEntryAdded(arg, { updateCachedData, cacheEntryRemoved }) {constws=newWebSocket('ws://localhost:8080')// populate the array with messages as they are received from the websocketws.addEventListener('message', (event) => {updateCachedData((draft) => {draft.push(JSON.parse(event.data)) }) })await cacheEntryRemovedws.close() }, }),// highlight-end }),})
Performing multiple requests with a single query
In the example below, a query is written to fetch all posts for a random user. This is done using a first request for a random user, followed by getting all posts for that user. Using queryFn allows the two requests to be included within a single query, avoiding having to chain that logic within component code.
// file: types.ts noEmitexportinterfacePost {}exportinterfaceUser { id:number}// file: api.tsimport { createApi, fetchBaseQuery, FetchBaseQueryError,} from'@reduxjs/toolkit/query'import { Post, User } from'./types'constapi=createApi({ baseQuery:fetchBaseQuery({ baseUrl:'/ ' }),endpoints: (build) => ({ getRandomUserPosts:build.query<Post,void>({asyncqueryFn(_arg, _queryApi, _extraOptions, fetchWithBQ) {// get a random userconstrandomResult=awaitfetchWithBQ('users/random')if (randomResult.error) throwrandomResult.errorconstuser=randomResult.data asUserconstresult=awaitfetchWithBQ(`user/${user.id}/posts`)returnresult.data? { data:result.data asPost }: { error:result.error asFetchBaseQueryError } }, }), }),})