Miscellaneous APIs

1. buildCreateApi(), coreModule, etc.

If you want to use different versions of useSelector or useDispatch for a custom context, you can do the following:

import * as React from 'react' import { createDispatchHook } from 'react-redux' import { buildCreateApi, coreModule, reactHooksModule, } from '@reduxjs/toolkit/query/react' const MyContext = React.createContext(null) const customCreateApi = buildCreateApi( coreModule(), reactHooksModule({ useDispatch: createDispatchHook(MyContext) }) )

If you want to create your own module, you should review the react-hooks module to see what an implementation would look like.:

import { CoreModule } from '@internal/core/module' import { BaseQueryFn, EndpointDefinitions, Api, Module, buildCreateApi, coreModule, } from '@reduxjs/toolkit/query' export const customModuleName = Symbol() export type CustomModule = typeof customModuleName declare module '../apiTypes' { export interface ApiModules { [customModuleName]: { endpoints: { [K in keyof Definitions]: { myEndpointProperty: string } } } } } export const myModule = (): Module => ({ name: customModuleName, init(api, options, context) { // initialize stuff here if you need to return { injectEndpoint(endpoint, definition) { const anyApi = (api as any) as Api, string, string, CustomModule | CoreModule > anyApi.endpoints[endpoint].myEndpointProperty = 'test' }, } }, }) export const myCreateApi = buildCreateApi(coreModule(), myModule())

2. baseQuery

Instead of using fetchBaseQuery(), you can define your own base query. eg.: const customBaseQuery = ( args, { signal, dispatch, getState }, extraOptions ) => { if (Math.random() > 0.5) return { error: 'Too high!' } return { data: 'All good!' } } You can use Axios: import { createApi } from '@reduxjs/toolkit/query' import axios from 'axios' const axiosBaseQuery = ({ baseUrl } = { baseUrl: '' }) => async ({ url, method, data, params }) => { try { const result = await axios({ url: baseUrl + url, method, data, params }) return { data: result.data } } catch (axiosError) { let 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' }), }), } }, }) You can use GraphQL   42622  : 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, }), }), }) You can wrap fetchBaseQuery() such that when encountering a 401 Unauthorized error, an additional request is sent to refresh an authorization token, and re-try to initial query after re-authorizing. 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 } You can use async-mutex to prevent multiple unauthorized errors: 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 exports a utility called 'retry' that you can wrap the baseQuery with. It defaults to 5 attempts with a basic exponential backoff.
The default behavior would retry at these intervals:
   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) 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 } = apiimport { 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 In addition to 'data' and 'error', a baseQuery can also include a 'meta' property in its return value: 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 ) }, }), }), }) You can construct a dynamic baseURL using a 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. 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 */

3. transformResponse

You can use transformResponse in conjunction with createEntityAdapter to normalize the data before storing it in the cache: 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, meta, arg) { return postsAdapter.addMany(postsAdapter.getInitialState(), response) }, }), }), }) export const { useGetPostsQuery } = api

4. queryFn

You can implement qureyFn to bypass baseQuery: const queryFn = ( args, { signal, dispatch, getState }, extraOptions, baseQuery ) => { if (Math.random() > 0.5) return { error: 'Too high!' } return { data: 'All good!' } } Wit queryFn, you can also perform multiple requests with a single query: 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) throw randomResult.error const user = randomResult.data const result = await fetchWithBQ(`user/${user.id}/posts`) return result.data ? { data: result.data } : { error: result.error } }, }), }), }) Using WebSocket, you can 'stream updates': 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() }, }), }), })

5. <ApiProvider>

If you do not already have a Redux store, you can use this: import * as React from 'react'; import { ApiProvider } from '@reduxjs/toolkit/query/react'; import { Pokemon } from './features/Pokemon'; function App() { return ( ); }

6. setupListeners

A utility used to enable refetchOnFocus and refetchOnReconnect behaviors. It requires the dispatch method from your store. Calling setupListeners(store.dispatch) will configure listeners with the recommended defaults, but you have the option of providing a callback for more granular control. let initialized = false export function setupListeners( dispatch: ThunkDispatch, customHandler?: ( dispatch: ThunkDispatch, actions: { onFocus: typeof onFocus onFocusLost: typeof onFocusLost onOnline: typeof onOnline onOffline: typeof onOffline } ) => () => void ) { function defaultHandler() { const handleFocus = () => dispatch(onFocus()) const handleFocusLost = () => dispatch(onFocusLost()) const handleOnline = () => dispatch(onOnline()) const handleOffline = () => dispatch(onOffline()) const handleVisibilityChange = () => { if (window.document.visibilityState === 'visible') { handleFocus() } else { handleFocusLost() } } if (!initialized) { if (typeof window !== 'undefined' && window.addEventListener) { // Handle focus events window.addEventListener( 'visibilitychange', handleVisibilityChange, false ) window.addEventListener('focus', handleFocus, false) // Handle connection events window.addEventListener('online', handleOnline, false) window.addEventListener('offline', handleOffline, false) initialized = true } } const unsubscribe = () => { window.removeEventListener('focus', handleFocus) window.removeEventListener('visibilitychange', handleVisibilityChange) window.removeEventListener('online', handleOnline) window.removeEventListener('offline', handleOffline) initialized = false } return unsubscribe } return customHandler ? customHandler(dispatch, { onFocus, onFocusLost, onOffline, onOnline }) : defaultHandler() } If you notice, onFocus, onFocusLost, onOffline, onOnline are all actions that are provided to the callback. Additionally, these actions are made available to api.internalActions and are able to be used by dispatching them like this: dispatch(api.internalActions.onFocus())