Skip to main content

RTK Query Basics

RTK Query is a data fetching tool built on top of Redux Toolkit. It makes API calls super easy and handles loading states, caching, and more automatically.

What RTK Query Does

  • Fetches data from APIs
  • Caches responses so you don't re-fetch the same data
  • Handles loading states (loading, success, error)
  • Auto-refetches when data gets stale
  • Optimistic updates for better UX

Basic Setup

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Create API slice
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
// Get all posts
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post'],
}),

// Get single post
getPost: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),

// Create new post
addPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
invalidatesTags: ['Post'],
}),
}),
})

// Export hooks
export const {
useGetPostsQuery,
useGetPostQuery,
useAddPostMutation,
} = apiSlice

Add to Store

import { configureStore } from '@reduxjs/toolkit'
import { apiSlice } from './api/apiSlice'

export const store = configureStore({
reducer: {
api: apiSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
})

Using in Components

Fetching Data

import { useGetPostsQuery } from './api/apiSlice'

function PostsList() {
const {
data: posts,
isLoading,
isSuccess,
isError,
error
} = useGetPostsQuery()

if (isLoading) return <div>Loading...</div>
if (isError) return <div>Error: {error.message}</div>

return (
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
)
}

Creating Data

import { useAddPostMutation } from './api/apiSlice'

function AddPost() {
const [addPost, { isLoading }] = useAddPostMutation()

const handleSubmit = async (e) => {
e.preventDefault()

try {
await addPost({
title: 'New Post',
content: 'This is my post content'
}).unwrap()

alert('Post created!')
} catch (error) {
alert('Failed to create post')
}
}

return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create Post'}
</button>
</form>
)
}

Query vs Mutation

Queries (GET data)

  • Use builder.query()
  • For fetching/reading data
  • Auto-run when component mounts
  • Return data, isLoading, error, etc.

Mutations (POST/PUT/DELETE)

  • Use builder.mutation()
  • For creating/updating/deleting data
  • Must be triggered manually
  • Return [trigger, result] array

Tags and Caching

Tags help control when data gets refetched:

endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
// This query provides 'Post' data
providesTags: ['Post'],
}),

addPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
// When this runs, refetch anything tagged 'Post'
invalidatesTags: ['Post'],
}),
})

Authentication

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

Error Handling

const apiSlice = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `/users/${id}`,
transformErrorResponse: (response, meta, arg) => {
return {
status: response.status,
message: response.data?.message || 'Something went wrong'
}
},
}),
}),
})

Transform Response

endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
// Transform API response before storing in cache
return response.data.map(post => ({
id: post.id,
title: post.title,
author: post.user.name, // Flatten nested data
createdAt: new Date(post.created_at), // Convert dates
}))
},
}),
})

Polling (Auto-refresh)

function LivePosts() {
const { data } = useGetPostsQuery(undefined, {
pollingInterval: 5000, // Refetch every 5 seconds
})

return <div>{/* render posts */}</div>
}

Skip Queries

function UserProfile({ userId }) {
const { data: user } = useGetUserQuery(userId, {
skip: !userId, // Don't run query if no userId
})

return user ? <div>{user.name}</div> : null
}

Key Benefits

  1. Less code - No need to write loading/error logic
  2. Automatic caching - Same requests are cached
  3. Background refetching - Keeps data fresh
  4. TypeScript support - Great type safety
  5. DevTools integration - See API calls in Redux DevTools

When to Use RTK Query

  • Making API calls
  • Need caching and background sync
  • Want automatic loading states
  • Working with REST APIs
  • Building data-heavy apps

RTK Query replaces libraries like React Query or SWR when you're already using Redux!