Skip to main content

RTK Query Advanced Features

Once you know the basics, RTK Query has powerful features for complex apps.

Optimistic Updates​

Update the UI immediately, then fix it if the server says no:

const apiSlice = createApi({
// ... config
endpoints: (builder) => ({
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch,
}),

// Update cache optimistically
onQueryStarted: async ({ id, ...patch }, { dispatch, queryFulfilled }) => {
// Update the cache immediately
const patchResult = dispatch(
apiSlice.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)

try {
await queryFulfilled
} catch {
// If request fails, undo the optimistic update
patchResult.undo()
}
},
}),
}),
})

Manual Cache Updates​

Sometimes you need to update the cache manually:

const PostEditor = ({ postId }) => {
const [updatePost] = useUpdatePostMutation()

const handleSave = async (postData) => {
try {
await updatePost({ id: postId, ...postData }).unwrap()

// Manually update related queries
dispatch(
apiSlice.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === postId)
if (post) {
Object.assign(post, postData)
}
})
)
} catch (error) {
console.error('Failed to update:', error)
}
}
}

Streaming Updates​

For real-time data that updates over time:

const apiSlice = createApi({
// ... config
endpoints: (builder) => ({
getNotifications: builder.query({
query: () => '/notifications',

// Set up real-time updates
onCacheEntryAdded: async (arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) => {
// Wait for initial data
await cacheDataLoaded

// Set up WebSocket
const ws = new WebSocket('ws://localhost:8080/notifications')

ws.addEventListener('message', (event) => {
const newNotification = JSON.parse(event.data)

updateCachedData((draft) => {
draft.unshift(newNotification)
})
})

// Clean up when cache entry is removed
await cacheEntryRemoved
ws.close()
},
}),
}),
})

Custom Base Query​

Create your own base query for special needs:

import { fetchBaseQuery } from '@reduxjs/toolkit/query'

const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
},
})

// Add retry logic
const baseQueryWithRetry = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions)

if (result.error && result.error.status === 401) {
// Try to refresh token
const refreshResult = await baseQuery('/auth/refresh', api, extraOptions)

if (refreshResult.data) {
// Store new token
api.dispatch(tokenReceived(refreshResult.data.token))

// Retry original request
result = await baseQuery(args, api, extraOptions)
} else {
api.dispatch(loggedOut())
}
}

return result
}

Code Splitting​

Split your API into multiple files:

// posts.js
import { apiSlice } from './apiSlice'

export const postsApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
}),
})

export const { useGetPostsQuery } = postsApiSlice
// users.js  
import { apiSlice } from './apiSlice'

export const usersApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
})

export const { useGetUsersQuery } = usersApiSlice

Conditional Queries​

Skip queries based on conditions:

function UserPosts({ userId, isLoggedIn }) {
const { data: posts } = useGetUserPostsQuery(userId, {
skip: !isLoggedIn || !userId,
})

return posts ? <PostList posts={posts} /> : null
}

Lazy Queries​

Trigger queries manually instead of automatically:

function SearchPosts() {
const [searchPosts, { data, isLoading }] = useLazyGetPostsQuery()

const handleSearch = (query) => {
searchPosts({ search: query })
}

return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{isLoading && <div>Searching...</div>}
{data && <PostList posts={data} />}
</div>
)
}

Multiple Requests​

Make several requests in parallel:

function Dashboard() {
const { data: user } = useGetCurrentUserQuery()
const { data: posts } = useGetPostsQuery()
const { data: notifications } = useGetNotificationsQuery()

// All three requests run at the same time

return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<NotificationsList notifications={notifications} />
</div>
)
}

Custom Hooks​

Create reusable hooks for complex logic:

function usePostWithComments(postId) {
const { data: post, ...postQuery } = useGetPostQuery(postId)
const { data: comments, ...commentsQuery } = useGetCommentsQuery(postId, {
skip: !post,
})

return {
post,
comments,
isLoading: postQuery.isLoading || commentsQuery.isLoading,
error: postQuery.error || commentsQuery.error,
}
}

// Usage
function PostDetail({ postId }) {
const { post, comments, isLoading, error } = usePostWithComments(postId)

if (isLoading) return <div>Loading...</div>
if (error) return <div>Error!</div>

return (
<div>
<h1>{post.title}</h1>
<Comments comments={comments} />
</div>
)
}

Error Handling Strategies​

function PostsList() {
const { data, error, isError, refetch } = useGetPostsQuery()

if (isError) {
if (error.status === 401) {
return <div>Please log in</div>
}

if (error.status === 500) {
return (
<div>
Server error. <button onClick={refetch}>Try again</button>
</div>
)
}

return <div>Something went wrong</div>
}

return <PostList posts={data} />
}

Cache Management​

Control how long data stays cached:

const apiSlice = createApi({
// ... config
keepUnusedDataFor: 60, // Keep data for 60 seconds after last use

endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
keepUnusedDataFor: 300, // Keep this specific data longer
}),
}),
})

Performance Tips​

  1. Use polling wisely - Don't poll too often
  2. Skip unused queries - Use skip parameter
  3. Transform responses - Keep only needed data
  4. Tag efficiently - Don't invalidate too much
  5. Lazy load - Use lazy queries for search/filters

Testing RTK Query​

import { setupApiStore } from '@reduxjs/toolkit/query/react'
import { apiSlice } from './apiSlice'

test('fetches posts', async () => {
const storeRef = setupApiStore(apiSlice)

const promise = storeRef.store.dispatch(apiSlice.endpoints.getPosts.initiate())

const result = await promise

expect(result.data).toEqual([
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
])
})

RTK Query handles most data fetching needs and scales well for large apps!