Back to Blog
React15 min read

Redux Toolkit RTK Query: Complete Guide

RTK Query is a powerful data fetching and caching library built on top of Redux Toolkit. It simplifies API data management in React applications by providing automatic caching, request deduplication, and optimistic updates. In this guide, we'll learn how to use RTK Query in an inventory management system.

RTK Query is a powerful data fetching and caching library built on top of Redux Toolkit. It simplifies API data management in React applications by providing automatic caching, request deduplication, and optimistic updates. In this guide, we'll learn how to use RTK Query in an inventory management system.

Installation

npm install @reduxjs/toolkit react-redux

Setting Up RTK Query API Slice

Creating a products API slice:

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

export const productsApiSlice = createApi({
  reducerPath: "products",
  baseQuery: fetchBaseQuery({ 
    baseUrl: "http://localhost:3000/api",
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as any).auth?.token;
      if (token) {
        headers.set("authorization", `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ["Product"],
  endpoints: (builder) => ({
    getProducts: builder.query({
      query: () => "/products",
      providesTags: (result) =>
        result?.data
          ? [
              ...result.data.map(({ id }: { id: string }) => ({ 
                type: "Product" as const, 
                id 
              })),
              { type: "Product", id: "LIST" },
            ]
          : [{ type: "Product", id: "LIST" }],
    }),
    getProductById: builder.query({
      query: (id: string) => "/products/" + id,
      providesTags: (result, error, id) => [{ type: "Product", id }],
    }),
    addProduct: builder.mutation({
      query: (productData) => ({
        url: "/products",
        method: "POST",
        body: productData,
        formData: true,
      }),
      invalidatesTags: [{ type: "Product", id: "LIST" }],
    }),
    updateProduct: builder.mutation({
      query: ({ id, formData }) => ({
        url: "/products/" + id,
        method: "PUT",
        body: formData,
        formData: true,
      }),
      invalidatesTags: (result, error, arg) => [
        { type: "Product", id: arg.id },
        { type: "Product", id: "LIST" },
      ],
    }),
    deleteProduct: builder.mutation({
      query: (id) => ({
        url: "/products/" + id,
        method: "DELETE",
      }),
      invalidatesTags: (result, error, id) => [
        { type: "Product", id },
        { type: "Product", id: "LIST" },
      ],
    }),
  }),
});

export const {
  useGetProductsQuery,
  useGetProductByIdQuery,
  useAddProductMutation,
  useUpdateProductMutation,
  useDeleteProductMutation,
} = productsApiSlice;

Configuring Redux Store

import { configureStore } from "@reduxjs/toolkit";
import { productsApiSlice } from "./products/productSlice";
import { categoriesApiSlice } from "./categories/categorySlice";

export const store = configureStore({
  reducer: {
    [productsApiSlice.reducerPath]: productsApiSlice.reducer,
    [categoriesApiSlice.reducerPath]: categoriesApiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(
      productsApiSlice.middleware,
      categoriesApiSlice.middleware
    ),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Using RTK Query Hooks

Using the generated hooks in components:

import { useGetProductsQuery, useDeleteProductMutation } from "../../state/products/productSlice";

function Products() {
  const { data, isLoading, isError, error } = useGetProductsQuery({});
  const [deleteProduct, { isLoading: isDeleting }] = useDeleteProductMutation();

  const handleDelete = async (id: string) => {
    try {
      await deleteProduct({ id }).unwrap();
      toast.success("Product deleted successfully");
    } catch (error) {
      toast.error("Failed to delete product");
    }
  };

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error loading products</div>;

  const products = data?.data || [];

  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <button onClick={() => handleDelete(product.id)} disabled={isDeleting}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

Advanced Features

Polling

const { data } = useGetProductsQuery(
  {},
  {
    pollingInterval: 5000, // Poll every 5 seconds
  }
);

Conditional Queries

const { data } = useGetProductByIdQuery(productId, {
  skip: !productId, // Skip query if productId is falsy
});

Best Practices

  • Use tag-based cache invalidation for automatic refetching
  • Implement optimistic updates for better UX
  • Use skip option for conditional queries
  • Configure baseQuery with authentication headers
  • Organize API slices by feature domain
  • Use unwrap() for mutation error handling
  • Implement proper error handling in components

Conclusion

RTK Query provides a powerful, type-safe solution for API data management in React applications. With automatic caching, request deduplication, and optimistic updates, it simplifies complex data fetching scenarios. This makes it perfect for inventory management systems and other data-heavy applications.