DAY 55: Mastering RTK Query – API Slice, Queries, Mutations, Caching & Optimistic Updates | ReactJS

Ritik KumarRitik Kumar
11 min read

πŸš€ Introduction

Welcome to Day 55 of my Web Development Journey!
After building a solid foundation in HTML, CSS, and JavaScript, I moved on to ReactJS β€” a powerful library for building dynamic and interactive user interfaces.

So far, I’ve worked with different state management techniques in React such as useState, Context API, useReducer, and even external libraries like Zustand. More recently, I explored Redux, one of the most popular and robust solutions for managing state in modern web applications.

Over the past few days, after strengthening my understanding of the core concepts of Redux Toolkit (RTK), I focused on RTK Query β€” a data-fetching and server state management tool that comes bundled with Redux Toolkit.

πŸ“‚ You can check out my complete Redux learning journey in my GitHub repository.

πŸ‘‰ I also share real-time updates and coding insights on Twitter.

I’m documenting this journey publicly to stay consistent and to share what I’ve learned. Whether you’re just starting with React or looking to strengthen your knowledge of state management and data fetching, I hope this blog adds value to your learning journey!

πŸ“… Here’s What I Covered Over the Last 3 Days

Day 52 – Getting Started with RTK Query

  • Why use RTK Query instead of manual data fetching?
  • Setting up an API Slice
  • Understanding Queries vs. Mutations
  • Using auto-generated hooks for API calls

Day 53 – Going Deeper into RTK Query

  • How caching works and why it’s powerful
  • Exploring Base Query for custom fetch logic
  • Adding Middleware for advanced use cases
  • Using Polling to keep data fresh

Day 54 – Advanced RTK Query Concepts

  • Handling Lazy Queries for on-demand fetching
  • Implementing Optimistic Updates for a smooth UX

Now, let’s break down each of these concepts with explanations and implementations πŸ‘‡


1. RTK Query

RTK Query is a powerful data-fetching and caching tool built into Redux Toolkit.
It eliminates the need to manually handle loading states, caching, re-fetching, and synchronization between client and server.

Key Points:

  • Built on top of Redux Toolkit
  • Handles server state (not just client state like useState or Redux slices)
  • Reduces boilerplate by auto-generating hooks for queries and mutations
  • Provides caching, invalidation, polling, and optimistic updates out of the box

Why Use It?

Normally, data fetching in React involves:

  • Writing useEffect for API calls
  • Maintaining loading, error, and data states
  • Handling re-fetching when dependencies change
  • Managing cache manually

With RTK Query, all of these are simplified. It:

  • Generates API hooks automatically
  • Caches data for you
  • Invalidates and refetches data when needed
  • Optimizes network requests (no duplicate calls for the same query)

Core Concepts:

Here is the core concepts of RTK Query

1. API Slice

An API Slice in RTK Query is the central place where we define all our API endpoints.
It is created using createApi, which requires:

  • A reducerPath (unique name for this API slice)
  • A baseQuery (how to fetch data, usually fetchBaseQuery)
  • endpoints (functions for queries & mutations)

Here’s an example with getPosts (query) and addPost (mutation):

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

// Define API slice
export const postsApi = createApi({
  reducerPath: "postsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com/" }),
  endpoints: (builder) => ({
    // Query - Fetch all posts
    getPosts: builder.query({
      query: () => "posts",
    }),
    // Mutation - Add a new post
    addPost: builder.mutation({
      query: (newPost) => ({
        url: "posts",
        method: "POST",
        body: newPost,
      }),
    }),
  }),
});

// Export auto-generated hooks
export const { useGetPostsQuery, useAddPostMutation } = postsApi;

Explanation:

  • createApi β†’ creates the API slice.
  • baseQuery β†’ defines base URL for all requests.
  • builder.query β†’ used for fetching data (GET).
  • builder.mutation β†’ used for modifying data (POST, PUT, DELETE).
  • RTK Query automatically generates hooks:
    • useGetPostsQuery() β†’ fetch posts
    • useAddPostMutation() β†’ add a new post

2. Queries vs Mutations

FeatureQueriesMutations
PurposeFetching data (read operations)Creating, updating, or deleting data
MethodUsually GETPOST, PUT, PATCH, DELETE
Auto-Cachingβœ… Yes (results are cached & reused)❌ No (they update data and usually invalidate cache)
React HookuseGetPostsQuery() (e.g., fetch posts)useAddPostMutation() (e.g., add a new post)
  • Queries are used to read data from the server. They support auto-caching, refetching, and are optimized for repeated fetches.
  • Mutations are used to modify server-side data (create, update, delete). They don’t cache results but can trigger cache invalidations to keep data fresh.

3. Hooks in RTK Query

RTK Query automatically generates custom React hooks for every query and mutation we define in our API slice.
These hooks handle fetching, loading states, errors, and caching out of the box.

useGetPostsQuery (Query Hook):
This hook is used for fetching posts (read operation).

const { data: posts, isLoading, isError } = useGetPostsQuery();
  • data β†’ contains the fetched posts
  • isLoading β†’ true while fetching
  • isError β†’ true if request fails

useAddPostMutation (Mutation Hook):
This hook is used for adding a new post (write operation).

const [addPost, { isLoading }] = useAddPostMutation();
  • Returns a mutation function (addPost)
  • Second item is an object with state β†’ isLoading, isError, etc.
  • Calling addPost() triggers the request.

4. Caching in RTK Query

One of the biggest advantages of RTK Query is its automatic caching mechanism.

  • Queries automatically cache results so if the same query is requested again, data is served from cache instead of making another network request.
  • To keep cached data fresh after mutations (like adding or deleting a post), we use providesTags and invalidatesTags.
// postsApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const postsApi = createApi({
  reducerPath: "postsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com" }),
  tagTypes: ["Posts"], // define tag type
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => "/posts",
      providesTags: ["Posts"], // cached under 'Posts' tag
    }),
    addPost: builder.mutation({
      query: (newPost) => ({
        url: "/posts",
        method: "POST",
        body: newPost,
      }),
      invalidatesTags: ["Posts"], // will refresh 'Posts' cache
    }),
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;

Using providesTags (for Queries)
providesTags tells RTK Query:

"This query provides data for this specific tag."

Using invalidatesTags (for Mutations)
invalidatesTags tells RTK Query:

"When this mutation succeeds, invalidate (clear) the cache for this tag so queries refetch fresh data."

In the example above:

  • getPosts β†’ provides cache for "Posts"
  • addPost β†’ invalidates "Posts" cache, so getPosts automatically refetches updated data

5. Base Query in RTK Query

The baseQuery defines how all requests in an API slice are made.

  • It usually uses fetchBaseQuery which is a small wrapper around the standard fetch API.
  • You can also customize it using prepareHeaders to set authentication tokens or other headers dynamically.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const postsApi = createApi({
  reducerPath: "postsApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "https://jsonplaceholder.typicode.com",
    prepareHeaders: (headers) => {
      const token = localStorage.getItem("token"); // example: read token from localStorage
      if (token) {
        headers.set("Authorization", `Bearer ${token}`);
      }
      return headers;
    },
  }),
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => "/posts",
    }),
  }),
});

export const { useGetPostsQuery } = postsApi;
  • baseUrl β†’ root URL for all API requests.
  • prepareHeaders β†’ runs before each request; useful for setting Authorization or Content-Type headers.
  • Token is fetched from localStorage and attached as Bearer token.
  • This makes authentication handling centralized β€” no need to repeat headers in every query/mutation.

6. Middleware in RTK Query

In Redux Toolkit, middleware sits between dispatching an action and the moment it reaches the reducer.
For RTK Query, the generated API slice automatically comes with its own middleware that:

  • Manages caching
  • Handles subscriptions and polling
  • Triggers automatic refetches
  • Keeps your data fresh

When setting up our store, we just need to include this middleware along with the default middleware.

import { configureStore } from "@reduxjs/toolkit";
import { postsApi } from "./postsApi"; // our API slice

export const store = configureStore({
  reducer: {
    [postsApi.reducerPath]: postsApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(postsApi.middleware),
});
  • postsApi.reducerPath β†’ registers the API slice reducer under its unique path.
  • postsApi.middleware β†’ enables caching, polling, and request lifecycle management.
  • concat() β†’ adds the RTK Query middleware alongside the default Redux middleware.
  • This ensures our API slice works correctly with caching and background updates.

7. Polling in RTK Query

Polling allows our app to automatically refetch data at a regular interval, keeping the UI in sync with the server without requiring manual refresh.
It’s very useful for real-time dashboards, notifications, or live feeds.

// Polls the server every 10 seconds (10000 ms)
  const { data, isLoading } = useGetPostsQuery(undefined, {
    pollingInterval: 10000,
  });
  • pollingInterval: 10000 β†’ refetches posts every 10 seconds.
  • RTK Query automatically cancels polling when the component unmounts or loses focus (to save resources).
  • Ideal for scenarios where we want fresh data without user interaction.

8. Lazy Queries in RTK Query

Lazy Queries let you trigger a query manually instead of running it automatically when a component mounts.
They are useful when data should only be fetched on demand (e.g., button click, form submission, search action).

const [trigger, { data, isLoading, isError }] = useLazyGetPostsQuery();
  • useLazyGetPostsQuery() β†’ returns a trigger function + query result state.
  • trigger() β†’ manually runs the query when called.
  • Perfect for search bars, button-triggered fetches, or conditional fetching where auto-fetching is not desired.

9. Optimistic Updates in RTK Query

Optimistic updates improve UX by immediately updating the UI before the server confirms the change.
If the request fails, RTK Query automatically rolls back the update, ensuring UI consistency.

How RTK Query Implements It?

RTK Query uses the onQueryStarted lifecycle method in mutations.

Syntax:

onQueryStarted(arg, { dispatch, queryFulfilled, getState })
  • arg β†’ The argument passed to the mutation (e.g., updated post data).
  • dispatch β†’ Used to optimistically update the cache.
  • queryFulfilled β†’ A promise that resolves when the server responds (success/failure).
  • getState β†’ Access to the current Redux state (optional).
Example: updatePost with Optimistic Updates
// postsApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const postsApi = createApi({
  reducerPath: "postsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com/" }),
  tagTypes: ["Posts"],
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => "posts",
      providesTags: ["Posts"],
    }),

    updatePost: builder.mutation({
      query: ({ id, ...patch }) => ({
        url: `posts/${id}`,
        method: "PUT",
        body: patch,
      }),
      async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        // Optimistic update: modify cached data before server response
        const patchResult = dispatch(
          postsApi.util.updateQueryData("getPosts", undefined, (draft) => {
            const post = draft.find((p) => p.id === id);
            if (post) {
              Object.assign(post, patch); // apply changes locally
            }
          })
        );

        try {
          await queryFulfilled; // wait for server response
        } catch {
          patchResult.undo(); // rollback if request fails
        }
      },
      invalidatesTags: ["Posts"],
    }),
  }),
});

export const { useGetPostsQuery, useUpdatePostMutation } = postsApi;
  • updatePost mutation β†’ Sends a PUT request to update a post.
  • onQueryStarted β†’ Lifecycle method used for optimistic updates.
  • updateQueryData β†’ Temporarily updates cached getPosts data.
  • patchResult.undo() β†’ Automatically reverts changes if the request fails.
  • invalidatesTags β†’ Ensures cache refetch if needed after success.

This creates a snappy UI where users see changes instantly, and the app gracefully handles errors.

  1. Create an API Slice using createApi() and define baseQuery.
  2. Define Endpoints (queries for GET, mutations for POST/PUT/DELETE).
  3. Export Auto-Generated Hooks from the API slice.
  4. Add API Reducer & Middleware to the Redux store.
  5. Use Queries in components for fetching data.
  6. Use Mutations in components for creating/updating/deleting data.
  7. Manage Cache with providesTags and invalidatesTags.
  8. Enhance with Features like Polling, Lazy Queries, and Optimistic Updates.

Final Thoughts:

  • RTK Query simplifies server-state management in React apps.
  • Reduces boilerplate compared to plain Redux or manual data fetching.
  • Handles data fetching, caching, and synchronization automatically.
  • Core concepts like API Slice, Queries/Mutations, Caching, Middleware, Polling, Lazy Queries, and Optimistic Updates give us complete control over API interactions.
  • Provides a clean, declarative, and efficient workflow.
  • Ideal for both small projects and large-scale applications.

Key takeaway: Focus more on building features, and let RTK Query handle the data flow.


2. What’s Next

I’m excited to keep growing and sharing along the way! Here’s what’s coming up:

  • Posting new blog updates every 3 days to share what I’m learning and building.
  • Diving deeper into Data Structures & Algorithms with Java β€” check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
  • Sharing regular progress and insights on X (Twitter) β€” feel free to follow me there and join the conversation!

Thanks for being part of this journey!


3. Conclusion

Learning RTK Query has been a game-changer in my React journey.
Coming from managing state with useState, Context API, Zustand, and even Redux slices, I can confidently say that RTK Query takes server-state management to another level.

Here’s why it stands out for me:

  • πŸš€ Boosts productivity β†’ No need to write repetitive useEffect + fetch + state handling logic.
  • πŸ”„ Keeps data fresh β†’ Automatic caching, invalidation, and background refetching.
  • 🎯 Improves UX β†’ Features like polling, lazy queries, and optimistic updates create smooth, responsive applications.
  • 🧩 Fits any scale β†’ Works equally well for small projects or enterprise-level apps.

For me, the biggest win was how effortlessly RTK Query integrates into Redux Toolkit, reducing boilerplate while giving full control over API interactions.

πŸ‘‰ If you’re still manually handling API calls in React, I highly recommend giving RTK Query a try β€” it will change the way you build apps.

Thanks for reading β€” and if you’re also learning Redux Toolkit, I hope this blog helps you speed up your journey

0
Subscribe to my newsletter

Read articles from Ritik Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ritik Kumar
Ritik Kumar

πŸ‘¨β€πŸ’» Aspiring Software Developer | MERN Stack Developer.πŸš€ Documenting my journey in Full-Stack Development & DSA with Java.πŸ“˜ Focused on writing clean code, building real-world projects, and continuous learning.