Example: error handling in component hook
import { usePostsQuery } from './services/api'
function PostDetail() {
const { data, error, isLoading } = usePostsQuery()
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
if ('status' in error) {
// accessing FetchBaseQueryError properties
const errMsg = 'error' in error ? error.error : JSON.stringify(error.data)
return (
<div>
<div>An error has occurred:</div>
<div>{errMsg}</div>
</div>
)
}
// accessing SerializedError properties
return <div>{error.message}</div>
}
if (data) {
return (
<div>
{data.map((post) => (
<div key={post.id}>Name: {post.name}</div>
))}
</div>
)
}
return null
}
This shows type narrowing using 'status' in error discriminator to safely access error properties.
Type guards for inline error handling
When handling errors inline after unwrapping a mutation call, thrown error has type any (TS < 4.4) or unknown (TS 4.4+). To safely access properties, narrow the type using a type predicate before accessing error properties.
Example: type predicates for error handling
import { FetchBaseQueryError } from '@reduxjs/toolkit/query'
/**
* Type predicate to narrow an unknown error to FetchBaseQueryError
*/
export function isFetchBaseQueryError(
error: unknown,
): error is FetchBaseQueryError {
return typeof error === 'object' && error != null && 'status' in error
}
/**
* Type predicate to narrow an unknown error to an object with a string message property
*/
export function isErrorWithMessage(
error: unknown,
): error is { message: string } {
return (
typeof error === 'object' &&
error != null &&
'message' in error &&
typeof (error as any).message === 'string'
)
}
These predicates can be used to safely narrow error types in catch blocks.
enhanceEndpoints purpose and usage
api.enhanceEndpoints returns an updated and enhanced version of the API slice object containing combined endpoint definitions. It is primarily useful for modifying an API definition generated from an API schema file like OpenAPI, adding hand-written configuration for cache invalidation management on top of generated endpoint definitions.
enhanceEndpoints example
const enhancedApi = api.enhanceEndpoints({
addTagTypes: ['User'],
endpoints: {
getUserByUserId: {
providesTags: ['User'],
},
patchUserByUserId: {
invalidatesTags: ['User'],
},
getUsers(endpoint) {
endpoint.providesTags = ['User']
endpoint.keepUnusedDataFor = 120
},
},
})
Code splitting with injectEndpoints
RTK Query allows dynamically injecting endpoint definitions into an existing API service object using api.injectEndpoints. This enables splitting up endpoints into multiple files for maintainability and lazy-loading endpoint definitions to reduce initial bundle sizes.
injectEndpoints signature and return value
api.injectEndpoints accepts a collection of endpoint definitions (same as createApi) and an optional overrideExisting parameter. It injects endpoints into the original API service object, modifying it immediately, and returns the same API service object reference. If using TypeScript, the return value has TS types for the new endpoints included, though it cannot modify the types for the original API reference.
overrideExisting parameter behavior
If you inject an endpoint that already exists and do not explicitly specify overrideExisting: true, the endpoint will not be overridden. In development mode, a warning is issued if overrideExisting is set to false, and an error is thrown if set to 'throw'.
Empty central API slice pattern
A typical code-splitting approach is to create one empty central API slice definition using createApi with endpoints: () => ({}) and a baseQuery. This empty API reference is then injected with endpoints in other files, ensuring endpoints are definitely injected when imported.
injectEndpoints example
const extendedApi = emptySplitApi.injectEndpoints({
endpoints: (build) => ({
example: build.query({
query: () => 'test',
}),
}),
overrideExisting: false,
})
export const { useExampleQuery } = extendedApi
enhanceEndpoints modifications
api.enhanceEndpoints can modify caching behavior by changing providesTags, invalidatesTags, and keepUnusedDataFor values. It accepts addTagTypes to add new tag types and an endpoints object where each key is an endpoint name and the value is either an object with property overrides or a function that receives and modifies the endpoint definition.
buildCreateApi function
You can create custom versions of createApi by calling buildCreateApi and passing module instances to it. This allows you to specify non-default options for modules or add your own custom modules.
Custom React-Redux hooks in RTK Query
To use custom versions of useSelector, useDispatch, and useStore (such as with a custom context), pass them to reactHooksModule via a hooks option when building a custom createApi. Use createDispatchHook, createSelectorHook, and createStoreHook from react-redux to create these custom hooks bound to your context.
RTK Query createApi variants
RTK Query includes two variants of createApi: createBaseApi, which contains only the UI-agnostic Redux logic (the core module), and createApi, which contains both the core and React hooks modules.
Custom React-Redux hooks example
import * as React from 'react'
import {
createDispatchHook,
createSelectorHook,
createStoreHook,
ReactReduxContextValue,
} from 'react-redux'
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'
const MyContext = React.createContext<ReactReduxContextValue | null>(null)
const customCreateApi = buildCreateApi(
coreModule(),
reactHooksModule({
hooks: {
useDispatch: createDispatchHook(MyContext),
useSelector: createSelectorHook(MyContext),
useStore: createStoreHook(MyContext),
},
}),
)
This example shows how to customize RTK Query to use custom React-Redux hooks bound to a custom context.
Custom createSelector for RTK Query
Both coreModule and reactHooksModule accept a createSelector option, which should be a selector creator instance from Reselect or with an equivalent signature. This allows you to customize the memoization strategy used by RTK Query selectors.
Custom createSelector with lruMemoize example
import * as React from 'react'
import { createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '@reduxjs/toolkit/query/react'
const createLruSelector = createSelectorCreator(lruMemoize)
const customCreateApi = buildCreateApi(
coreModule({ createSelector: createLruSelector }),
reactHooksModule({ createSelector: createLruSelector }),
)
This example shows how to customize RTK Query to use LRU memoization for selectors instead of the default memoization strategy.
Creating a custom RTK Query module
To create a custom module, define a function that returns a Module object with a name property (typically a Symbol), and an init method that receives the api, options, and context. The init method should return an object with an injectEndpoint method that receives the endpoint name and definition. Use buildCreateApi with coreModule and your custom module to create a custom createApi.
Custom module declaration merging
When creating a custom RTK Query module, use TypeScript declaration merging to extend the ApiModules interface. Define the module Symbol as a new key in ApiModules, specifying the structure of properties that will be added to endpoints by your module.
Custom RTK Query module example
import {
BaseQueryFn,
CoreModule,
EndpointDefinitions,
Api,
Module,
buildCreateApi,
coreModule,
} from '@reduxjs/toolkit/query'
export const customModuleName = Symbol()
export type CustomModule = typeof customModuleName
declare module '@reduxjs/toolkit/query' {
export interface ApiModules<
BaseQuery extends BaseQueryFn,
Definitions extends EndpointDefinitions,
ReducerPath extends string,
TagTypes extends string,
> {
[customModuleName]: {
endpoints: {
[K in keyof Definitions]: {
myEndpointProperty: string
}
}
}
}
}
export const myModule = (): Module<CustomModule> => ({
name: customModuleName,
init(api, options, context) {
return {
injectEndpoint(endpoint, definition) {
const anyApi = api as any as Api<
any,
Record<string, any>,
string,
string,
CustomModule | CoreModule
>
anyApi.endpoints[endpoint].myEndpointProperty = 'test'
},
}
},
})
export const myCreateApi = buildCreateApi(coreModule(), myModule())
This example shows how to create a custom RTK Query module that adds a myEndpointProperty to all endpoints.
RTK Query request handling overview
RTK Query is agnostic to request libraries and uses baseQuery and query options to handle requests. baseQuery is a common function processing endpoint queries, defaulting to fetchBaseQuery (a lightweight fetch wrapper). For custom behavior, wrap or replace baseQuery, or use queryFn for endpoint-specific logic. transformResponse and transformErrorResponse manipulate cached data/errors before storage.
RTK Query replaces createAsyncThunk-based data fetching
RTK Query is purpose-built to solve the use case of data fetching and should eliminate the need for most hand-written side effects logic using createAsyncThunk. It covers overlapping behavior including caching, request lifecycle management with states like isUninitialized, isLoading, and isError.
Migrating from createAsyncThunk involves removing slice, thunk, and custom hook code
When migrating from createAsyncThunk and createSlice to RTK Query, the appropriate endpoints should be added to an RTK Query API slice and previous feature code deleted. This generally will not include much common code between the two approaches, as the tools work differently and one will replace the other. In the Pokemon example, RTK Query eliminated 85+ lines of boilerplate code (slice file with thunk, selectors, and custom hook) and replaced it with less than 20 lines of API definition.
RTK Query prevents duplicate requests across multiple components
RTK Query automatically handles de-duping requests on a granular level to prevent sending unnecessary duplicate requests. Multiple components rendering simultaneously that call the same generated hook will not each send off separate requests; RTK Query manages this internally.
Adding RTK Query API to store with reducerPath and middleware
To connect an RTK Query API slice to the store, add the api.reducer under the api.reducerPath key in the reducers object, and add the api.middleware to the middleware chain using gDM().concat(api.middleware). This enables the store to process internal actions, allows generated API logic to find state correctly, and adds logic for managing caching, invalidation, subscriptions, polling, and more.
Example store configuration:
```ts
import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
```
RTK Query auto-generated hooks have identical usage to custom hooks
The auto-generated hook from RTK Query follows the same interface as a custom hook built with Redux and useSelector. For example, useGetPokemonByNameQuery returns an object with properties like data, isError, isLoading, isUninitialized, and isSuccess. Components can be migrated simply by changing the import path from a custom hook to the RTK Query API export.
extractRehydrationInfo option for persistence
RTK Query supports rehydration via the `extractRehydrationInfo` option on `createApi`. This function is passed every dispatched action, and where it returns a value other than `undefined`, that value is used to rehydrate the API state for fulfilled and errored queries.
Redux Persist rehydration implementation example
Example of using extractRehydrationInfo with Redux Persist:
```ts
import type { Action } from '@reduxjs/toolkit'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { REHYDRATE } from 'redux-persist'
type RootState = any
function isHydrateAction(action: Action): action is Action<typeof REHYDRATE> & {
key: string
payload: RootState
err: unknown
} {
return action.type === REHYDRATE
}
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
extractRehydrationInfo(action, { reducerPath }): any {
if (isHydrateAction(action)) {
// when persisting the api reducer
if (action.key === 'key used with redux-persist') {
return action.payload
}
// When persisting the root reducer
return action.payload[api.reducerPath]
}
},
endpoints: (build) => ({
// omitted
}),
})
```
This shows how to detect Redux Persist REHYDRATE actions and extract the appropriate payload based on whether the API reducer or root reducer is being persisted.
Redux Persist integration with RTK Query
API state rehydration can be used in conjunction with Redux Persist by leveraging the `REHYDRATE` action type imported from `redux-persist`. This can be used out of the box with the `autoMergeLevel1` or `autoMergeLevel2` state reconcilers when persisting the root reducer, or with the `autoMergeLevel1` reconciler when persisting just the API reducer.
React hooks handle subscription lifecycle automatically
When using React hooks, the subscription and unsubscription behavior is automatically handled by useQuery, useQuerySubscription, useLazyQuery, useLazyQuerySubscription, and useMutation hooks. These hooks abstract away the manual dispatch of initiate and calling unsubscribe.
RTK Query is UI-agnostic core
RTK Query's primary functionality is UI-agnostic and can be used with any UI layer, not just React. The library itself uses plain JS logic and can be used with React Class components and independently of React itself.