Redux Toolkit RTK Query – Complete Guide
Learn RTK Query for React data fetching and caching. Complete guide with API setup, cache management, mutations, and best practices. Step-by-step tutorial with examples.
Managing API data in React applications used to be a pain. I'd write custom hooks for fetching, manually handle loading states, implement caching logic, and deal with race conditions. Then I discovered RTK Query, and it eliminated about 80% of the boilerplate code I was writing for data fetching.
RTK Query is built on top of Redux Toolkit, but you don't need to understand Redux to use it. It provides automatic caching (so you don't refetch data unnecessarily), request deduplication (multiple components requesting the same data only triggers one API call), and optimistic updates (UI updates immediately while the request is in flight). It's like having a smart data fetching layer that handles all the edge cases for you.
In this guide, I'll walk you through using RTK Query in a real application. We'll set up an API slice, create query endpoints for fetching data, implement mutations for creating and updating records, handle cache invalidation, and manage loading and error states. By the end, you'll understand why RTK Query has become my go-to solution for API data management in React applications.
Installation
npm install @reduxjs/toolkit react-reduxSetting 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.
Related Articles
TanStack Table Implementation in React: Complete Guide
Build advanced data tables with sorting, filtering, pagination, and row selection.
React Hook Form with Zod Validation: Complete Guide
Learn how to implement form validation in React using React Hook Form and Zod.
TypeScript with React: Best Practices and Patterns
Learn TypeScript best practices for React development with type definitions and interfaces.
React Router Setup: Complete Guide for React Applications
Learn how to set up React Router DOM with routes, navigation, and protected routes.