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
- Use polling wisely - Don't poll too often
- Skip unused queries - Use
skipparameter - Transform responses - Keep only needed data
- Tag efficiently - Don't invalidate too much
- 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!