RTK Query in React-Redux Application: Understanding it for handling HTTP calls and perform Query and Mutation
RTK Query is a powerful data fetching and caching tool built provided in Redux Toolkit. It eliminates the need to manually write thunks middlewares or any other middlewares like SAGA, as well as eliminated need for creating reducers, and selectors for server-side data. In this article, I walk through an example that reads and writes Category data from a REST API step by step.
API Base URL used for this demo:
https://myserver.dataservices.net/api/CategoryRead, for Read Operations and
https://myserver.dataservices.net/api/CategoryWrite Operations
Step 1: Install Packages
We will create the React application using the Vite + React + TypeScript project template and install the required packages as shown in following Listing:
# Create Vite project
npm create vite@latest react-jan2026-app -- --template react-ts
cd react-jan2026-app
# Install Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
These are the two packages we need. The @reduxjs/toolkit includes RTK Query built-in, and react-redux provides the Provider component that connects the Redux store to your React tree.
Step 2: Define the Data Models
Make sure that before creating the API slice, define TypeScript models that represent the shape of the data returned by the API. Create the following files inside src/models/. as shown in the following Listing
File: src/models/category.ts
export class Category {
public CategoryRecordId: number;
public CategoryId: string;
public CategoryName: string;
public BasePrice: number;
constructor(
CategoryRecordId: number,
CategoryId: string,
CategoryName: string,
BasePrice: number
) {
this.CategoryRecordId = CategoryRecordId;
this.CategoryId = CategoryId;
this.CategoryName = CategoryName;
this.BasePrice = BasePrice;
}
}
Since the response is encapsulated in the ResponseObject, let's add ResponseObject class as showin the following Listing:
File: src/models/responseobject.ts
export class ResponseObject<T> {
public Records!: T[];
public Record!: T;
public StatusCode!: number;
public Message!: string;
}
Step 3: Create the API Slice
Let's create the API slice in the apislice.ts file. The API slice is the heart of RTK Query. It defines the base URL, all endpoints for queries and mutations, and automatically generates React hooks. Create this file at src/components/usingrtkquery/apislice.ts.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { Category } from "../../models/category";
import { ResponseObject } from "../../models/responseobject";
// Create the API Slice using createApi()
export const apiSlice = createApi({
reducerPath: 'api', // Implicitly created reducer used to mutate the store
baseQuery: fetchBaseQuery({
baseUrl: "https://coreapiforreact-aed8a3azbaeba6ep.eastus-01.azurewebsites.net/api"
}),
tagTypes: ["Categories"],
// Creating Middleware implicitly
endpoints: (builder) => ({
// GET endpoint — fetches all categories
getCategories: builder.query<Category[], void>({
query: () => 'CategoryRead', // resolves to baseUrl/CategoryRead
// Transform the API envelope to just the Records array
transformResponse: (response: ResponseObject<Category>) => response.Records,
providesTags: ["Categories"]
}),
// POST endpoint — creates a new category
addCategory: builder.mutation<ResponseObject<Category>, Partial<Category>>({
query: (cat: Category) => ({
url: "CategoryWrite",
contentType: 'application/json',
method: 'POST',
body: cat
}),
invalidatesTags: ["Categories"] // Cache is invalidated on successful POST
})
})
});
// Export the auto-generated hooks for use in components
export const { useGetCategoriesQuery, useAddCategoryMutation } = apiSlice;
Let;s understand the Key Concept of the API Slice
| Property Name | Uses of the property |
|---|---|
reducerPath: 'api' |
The key under which RTK Query registers its reducer to manipulate the Redux store with mutation. |
fetchBaseQuery |
This is a lightweight wrapper around fetch object. This sets the base URL for all requests to the REST API for Data operations. |
tagTypes: ["Categories"] |
This declares cache tags used to link queries and mutations for automatic invalidation after performing HTTP Calls. |
builder.query |
This defines a GET-style endpoint. This automatically generates a use[METHODNAE]Query hook. E.g. if the method name is getProperties, then the hook will be useGetPropertiesQuery. |
transformResponse |
This is used to extract the response.Records from the HTTP response from the API envelope before storing in cache |
providesTags |
This marks this query's cached data with the "Categories" tag. |
builder.mutation |
Defines a POST/PUT/DELETE endpoint; generates a use[METHOD-NAME]Mutation hook. E.g. if the method name is postProperties, then the hook generated will be usePostPropertiesMutation. |
invalidatesTags |
After a successful POST (or PUT and DELETE) automatically re-fetches all queries tagged "Categories" to update it to the store. |
Step 4: Create the Component Files
To perform Read and Write operations, we create two components inside src/components/usingrtkquery/ folder, one for listing categories and one for creating a new category.
Component 1 — File: categorylistcomponent.tsx
This component uses the auto-generated useGetCategoriesQuery hook to fetch and display all categories in a table. The code of the component is shown in the following listing:
import { useGetCategoriesQuery } from "./apislice";
const CategoryListComponent = () => {
// Destructure query state from the auto-generated hook
const { data: Records, isLoading, isFetching, error } = useGetCategoriesQuery();
if (isLoading) return <div>Loading data .......</div>;
if (error) return <div>Error Occurred......</div>;
if (isFetching) return <div>Data fetch is started....</div>;
return (
<div>
<h2>Category List Object</h2>
<table>
<thead>
<tr>
<th>Category Record Id</th>
<th>Category Id</th>
<th>Category Name</th>
<th>Base Price</th>
</tr>
</thead>
<tbody>
{
Array.isArray(Records) &&
Records.map((cate, index) => (
<tr key={index}>
<td>{cate.CategoryRecordId}</td>
<td>{cate.CategoryId}</td>
<td>{cate.CategoryName}</td>
<td>{cate.BasePrice}</td>
</tr>
))
}
</tbody>
</table>
</div>
);
};
export default CategoryListComponent;
The code works as follows:
useGetCategoriesQuery() fires the GET request automatically when the component mounts. It returns isLoading (first load), isFetching, the subsequent background refetches, error, and data that is aliased as Records. The Array.isArray() guard ensures safe rendering even if the data is momentarily undefined.
Component 2 — File: createcategorycomponent.tsx
This component uses the auto-generated useAddCategoryMutation() hook to POST a new category to the API. The code for the component is shown in the following Listing:
import { useAddCategoryMutation } from "./apislice";
import { Category } from "../../models/category";
import { useState } from "react";
const CategoryCreateComponent = () => {
// Local state pre-populated with a default Category object
const [category, setCategory] = useState<Category>(
new Category(0, 'Cat-8904', 'My Category New', 134444)
);
// Destructure the mutation trigger function from the hook
const [addCategory] = useAddCategoryMutation();
const save = () => {
// Trigger the POST mutation with partial category fields
addCategory({
CategoryId: category.CategoryId,
CategoryName: category.CategoryName,
BasePrice: category.BasePrice
});
};
return (
<div>
<div>
<button onClick={save}>Save</button>
</div>
<br />
</div>
);
};
export default CategoryCreateComponent;
How this code works: useAddCategoryMutation() returns a tuple — the first element is the trigger function (addCategory). Calling addCategory({...}) fires the POST request. Because the mutation declares invalidatesTags: ["Categories"] in the slice, RTK Query automatically re-fetches the category list the moment this POST succeeds — no manual state updates required.
Step 5: Wire Everything Together in
Let's modify the main.tsx in the application, the entry point of the application creates the Redux store, registers the API slice's reducer and middleware, and wraps the component tree in a Provider.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
// 1. Import configureStore to create the store configuration using the reducers
import { configureStore } from '@reduxjs/toolkit'
// 2. Import the Provider component from react-redux
// This bridges the React DOM tree with the Redux Object Model
// Provider creates a context subscription so all views can access the store
import { Provider } from 'react-redux'
// 3. Import the API Slice created using createApi()
import { apiSlice } from './components/usingrtkquery/aplislice'
// 3a. Import components that consume hooks from the API slice
import CategoryListComponent from './components/usingrtkquery/categorylistcomponent'
import CategoryCreateComponent from './components/usingrtkquery/createcategorycomponent'
// 4. Configure the store — register the API slice reducer and middleware
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware)
});
// 5. Mount the application — wrap components in Provider to expose the store
createRoot(document.getElementById('root')!).render(
<StrictMode>
<h3>The React-Redux App using ReduxJs Toolkit with RTK Query</h3>
<Provider store={store}>
<CategoryCreateComponent />
<hr />
<CategoryListComponent />
</Provider>
</StrictMode>,
)
Some Important Points of the main.tsx
- The
apiSlice.reducermust be registered in the store'sreducermap underapiSlice.reducerPath. - The
apiSlice.middlewaremust be added viagetDefaultMiddleware().concat(...), thishandles cache lifecycle, polling, and invalidation. - Both components must be children of
<Provider store={store}>to access the Redux store.
The following table explains how the Data Flow Works
| Action | What role the RTK Query plays |
|---|---|
CategoryListComponent mounts |
useGetCategoriesQuery() fires GET /CategoryRead, stores result in Redux cache |
CategoryListComponent re-mounts |
This returns cached data instantly, no new network request is made |
| User clicks Save | addCategory({...}) fires POST /CategoryWrite |
| POST succeeds | invalidatesTags: ["Categories"] triggers automatic re-fetch of the category list |
transformResponse runs |
Unwraps response.Records from the API envelope before caching |
The following table explain advantages of RTK Query
| Advantage | Description |
|---|---|
| Zero Boilerplate | We do not need manual thunks, loading states, or reducers needed for server data |
| Auto-generated Hooks | Every endpoint defined in api slice instantly produces a typed, ready-to-use React hook |
| Intelligent Caching | RTK Query deduplicates requests; same query called multiple times hits the network only once. This is amazing feature. |
| Tag-based Invalidation | Mutations auto-refresh linked queries is the reason for no manual refetch calls needed |
| transformResponse | RTK Query cleanly unwraps API envelopes before data reaches the component |
| Redux DevTools | RTK Query provides the full visibility into cache state and API lifecycle events |
| TypeScript First | In RTK Query we get the generics on builder.query<ReturnType, ArgType> give full type safety |
| No Extra Dependencies | Since RTK Query ships inside @reduxjs/toolkit no extra packages required to install |
Following table explains the limitations of RTK Query
| Limitation | Description |
|---|---|
| Requires Redux | Cannot be used without a Redux store; not suitable for Redux-free projects |
| Tag Complexity | Managing many tag types across large APIs can become difficult to reason about |
| No Built-in Pagination | Cursor-based or infinite scroll pagination must be implemented manually |
| REST-centric | GraphQL and WebSocket support requires custom baseQuery configuration |
| Global Cache Only | All cache is in the Redux store; no component-local caching support |
| Steep Learning Curve | The providesTags, invalidatesTags, transformResponse, and onQueryStarted require time to master |
Let's compare the RTK Query and TanStack Query
Both RTK Query and TanStack Query, formerly React Query solve the same problem i.e the efficient server-state management in React. However, they have different approaches. The following table shows a comprehensive side-by-side comparison:
| Feature / Aspect | RTK Query | TanStack Query |
|---|---|---|
| State Management | Stored in Redux store | Internal React context does not need Redux |
| Redux Dependency | Required reduxjs toolkit package | Not required any third party packages |
| Setup Complexity | Medium with store, reducer, middleware | Very Low because we wrap the app in QueryClientProvider |
| Endpoint Definition | Define as Centralized in API slice using the createApi |
Inline definition at usage site using useQuery and useMutation |
| Hook Generation | Hooks are Auto-generated per endpoint | Provides Generic hooks useQuery, useMutation |
| Cache Invalidation | Tag-based using providesTags and invalidatesTags |
Query key-based using queryClient.invalidateQueries |
| Automatic Refetch on Mutation | Available via tag invalidation | Available with via invalidateQueries in onSuccess |
| Pagination / Infinite Scroll | Manual implementation required | Built-in support using useInfiniteQuery |
| Optimistic Updates | Avaialble via onQueryStarted |
Available via onMutate and cancelQueries |
| Response Transformation | Supported Built-in using transformResponse |
Available via select option or inside queryFn |
| Polling | pollingInterval option |
refetchInterval option |
| Conditional Fetching | skip option |
enabled option |
| DevTools | Redux DevTools browser extension | TanStack Query Devtools |
| TypeScript Support | First-class with generics on endpoints | First-class with generics on hooks |
| GraphQL Support | Custom baseQuery needed |
Works via custom queryFn or plugins |
| Framework Support | React only via react-redux | React, Vue, Svelte, Solid, Angular |
| SSR Support | Available via initiate and getRunningQueriesThunk |
Available via prefetchQuery and dehydrate / hydrate |
| Bundle Size | ~15 KB (part of Redux Toolkit) | ~13 KB (standalone) |
| Client + Server State | Unified in Redux store | Server state only; use Zustand/Redux for client state |
| Best For | Projects already using Redux Toolkit | Projects wanting a lightweight, framework-agnostic solution |
Let's see when to use RTK Query and TanStack Query?
| Use RTK Query | Use TanStack Query |
|---|---|
| When your project already uses Redux Toolkit | When your project does not use Redux |
| When your project want client and server state in one store | When your project want to keep server state separate from client state |
| When your APIs follow a consistent REST pattern | When your project need infinite scroll or cursor-based pagination |
Now Run the application using tthe following command:
Conclusion
RTK Query is a powerful, zero-boilerplate solution for server state management in React applications that already use Redux.