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


The Schema for Category Data Returned is as follows:

{
  "Records": [
    {
      "CategoryRecordId": 84,
      "CategoryId": "",
      "CategoryName": "",
      "BasePrice": 0,
      "Products": null
    },
     .......
  ],
  "StatusCode": 200,
  "Message": "Categories are read successfully"
}
 


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.reducer must be registered in the store's reducer map under apiSlice.reducerPath.
  • The apiSlice.middleware must be added via getDefaultMiddleware().concat(...), this handles 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:

npm run dev

When you browse the http://localhost:5173, the Category list will be displayed as shown in following image:


Here, we can see the Redux Store with the getCategories query. Click on the Save button, the new record will be added, and List will be immediately refreshed as shown in the following figure:

We can see the Cat-8914 record added.


Conclusion

RTK Query is a powerful, zero-boilerplate solution for server state management in React applications that already use Redux.

 

Popular posts from this blog

ASP.NET Core 8: Creating Custom Authentication Handler for Authenticating Users for the Minimal APIs

ASP.NET Core 7: Using PostgreSQL to store Identity Information

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database