RTK Query Setup and Configuration
RTK Query is Redux Toolkit's official data-fetching solution that eliminates manual cache management by codifying fetch patterns as API endpoints. Setup requires three steps: configure your Redux store with RTK Query's reducer and middleware, define an API slice with typed endpoints, and integrate DevTools for debugging. Unlike SWR's implicit caching, RTK Query makes caching explicit and traceable, trading initial boilerplate for runtime predictability and powerful tools.
Step 1: Install Dependencies
RTK Query ships inside Redux Toolkit, so install Redux Toolkit and React-Redux:
npm install @reduxjs/toolkit react-redux
You'll also want Redux DevTools in development:
npm install --save-dev redux-devtools-extension
Step 2: Create the Redux Store
Create a file (e.g., store.ts) that configures Redux with RTK Query's reducer and middleware:
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query/react';
import { api } from './api';
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
setupListeners(store.dispatch);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
The key lines:
[api.reducerPath]: api.reducerregisters RTK Query's cache reducer into your storeapi.middlewareintercepts actions to manage cache lifecyclesetupListeners()enables automatic revalidation on focus/reconnect
Step 3: Wrap Your App with Redux Provider
In your app entry point:
import { Provider } from 'react-redux';
import { store } from './store';
export default function App() {
return (
<Provider store={store}>
<Main />
</Provider>
);
}
Step 4: Define Your First API Slice
Create an api.ts file with your endpoints. An API slice is a collection of typed endpoints:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
interface User {
id: number;
name: string;
email: string;
}
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
}),
getUserById: builder.query<User, number>({
query: (userId) => `/users/${userId}`,
}),
createUser: builder.mutation<User, Partial<User>>({
query: (newUser) => ({
url: '/users',
method: 'POST',
body: newUser,
}),
}),
}),
});
export const {
useGetUsersQuery,
useGetUserByIdQuery,
useCreateUserMutation,
} = api;
Breaking it down:
createApi()initializes the APIreducerPath: 'api'matches the store registrationfetchBaseQuery()wrapsfetch()with common options (base URL, headers, error handling)builder.query()defines read operations (GET)builder.mutation()defines write operations (POST, PUT, DELETE)- Generic types
<ResponseType, ArgType>enable TypeScript inference
Step 5: Use the Hooks in Components
RTK Query auto-generates hooks from your endpoint definitions. In any component under the Provider:
import { useGetUsersQuery, useCreateUserMutation } from './api';
export function UserList() {
const { data, isLoading, error } = useGetUsersQuery();
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error fetching users</div>;
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export function AddUserForm() {
const [createUser, { isLoading }] = useCreateUserMutation();
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await createUser({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isLoading}>
Add User
</button>
</form>
);
}
Advanced Configuration: Tags and Cache Invalidation
RTK Query uses tags to manage relationships between queries and mutations. Tag endpoints so mutations automatically revalidate related queries:
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['User'],
}),
createUser: builder.mutation<User, Partial<User>>({
query: (newUser) => ({
url: '/users',
method: 'POST',
body: newUser,
}),
invalidatesTags: ['User'], // Revalidates getUsers after mutation
}),
getUserById: builder.query<User, number>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
updateUser: builder.mutation<User, { id: number; updates: Partial<User> }>({
query: ({ id, updates }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: updates,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'User', id },
'User',
],
}),
}),
});
Tags with IDs (e.g., { type: 'User', id }) enable fine-grained invalidation—updating a single user only refetches that user's detail query, not the entire list.
Configuring Request Headers and Error Handling
Extend fetchBaseQuery with headers and error handling:
const baseQuery = fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
validateStatus: (response, body) => {
return response.status >= 200 && response.status < 300;
},
});
export const api = createApi({
reducerPath: 'api',
baseQuery,
endpoints: (builder) => ({
// ... endpoints
}),
});
Enabling Redux DevTools
Redux DevTools let you time-travel through state changes and inspect actions. Install the browser extension and it auto-connects. In your store config above, DevTools will show every RTK Query action, cache update, and state transition.
To explicitly configure in development:
import { composeWithDevTools } from 'redux-devtools-extension';
const store = configureStore({
reducer: { /* ... */ },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
enhancers: (getDefaultEnhancers) =>
process.env.NODE_ENV === 'development'
? getDefaultEnhancers().concat(composeWithDevTools())
: getDefaultEnhancers(),
});
Key Takeaways
- RTK Query requires Redux; configure the store, register the reducer and middleware, and wrap your app with
Provider - Define API slices with queries (GET) and mutations (POST, PUT, DELETE) using builder pattern
- Auto-generated hooks (
useGetUsersQuery,useCreateUserMutation) replace manual data fetching - Tags enable automatic cache invalidation based on mutation relationships
- Redux DevTools provide full visibility into API calls and cache state
Frequently Asked Questions
Do I have to use TypeScript with RTK Query?
No, but you should. RTK Query's types are automatic: once you type your endpoint response and arguments, all hook return types and arguments are inferred. JavaScript works, but you lose this benefit.
What if my API doesn't follow REST conventions?
RTK Query's query function is flexible. Return any object with url, method, body, etc. For GraphQL, you'd do:
query: (variables) => ({
url: '/graphql',
method: 'POST',
body: { query: GET_USERS, variables },
})
Can I use both RTK Query and SWR in the same app?
Yes, but it's messy. Each manages its own cache, so you risk inconsistency. Pick one for data fetching and stick with it.
How do I handle pagination with RTK Query?
Define separate queries for each page, or use the serializeQueryArgs option to merge pagination results into one cache entry. RTK Query v2 includes built-in utilities for this.
Does RTK Query work offline?
Not built-in. You'd need to persist the Redux store and manually manage offline detection. TanStack Query has better offline-first libraries.