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!