RTK Query or createSlice with Thunks: Choosing a Data-Fetching Pattern
RTK Query handles loading, error, and cache state for server data automatically, while createSlice with thunks fits client-only logic. This post compares both patterns with a working products API example.
03 Jan 2026, 21:01 UTC

The problem: server state keeps leaking into your Redux slices
When a React feature needs remote data, teams often add loading, error, and data fields to a Redux slice, then write thunks to fetch, set flags, and handle race conditions. The slice grows quickly, and every component reading that slice can re-render when any field changes. Cache invalidation, request deduplication, and background refetches must be hand-written.
Thesis: match the tool to the data source
Use RTK Query for REST-like server data that benefits from caching and automatic request tracking. Use createSlice with redux-thunk for client-only workflows, multi-step mutations, or non-HTTP sources such as WebSockets. RTK Query is part of @reduxjs/toolkit and generates React hooks, so you rarely write manual loading or error state.
Worked example: a products API slice
Run this in the project root. It assumes a Vite or Create React App project with React and TypeScript already installed.
# Install Redux Toolkit and React Redux
npm install @reduxjs/toolkit react-redux
Create src/features/products/productsApi.ts. Replace https://api.example.com with your real API base URL.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export interface Product {
id: string;
name: string;
price: number;
}
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query({
query: () => '/products',
providesTags: ['Product'],
}),
addProduct: builder.mutation>({
query: (body) => ({
url: '/products',
method: 'POST',
body,
}),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useAddProductMutation } = productsApi;
Add the reducer and middleware to the store in src/app/store.ts:
import { configureStore } from '@reduxjs/toolkit';
import { productsApi } from '../features/products/productsApi';
export const store = configureStore({
reducer: {
[productsApi.reducerPath]: productsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productsApi.middleware),
});
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
Provide the store with Provider in your entry file, then call the hook in a component:
const { data, isLoading, isError } = useGetProductsQuery();
Verifying cache behavior
- Open the app in Chrome with the Redux DevTools extension installed.
- Trigger
useGetProductsQueryby mounting the component. - In DevTools, look for
productsApi/initiateandproductsApi/fulfilledactions. The state should live underproductsApi, not a hand-written slice. - Mount and unmount the component twice within the cache lifetime. The Network tab should show only one request because RTK Query deduplicates identical queries.
- Call
useAddProductMutationand confirm DevTools shows an invalidation that triggers a refetch ofgetProducts.
If you do not see productsApi.middleware registered, queries will not cache or invalidate correctly. The middleware is required.
Trade-off: abstraction and debugging
RTK Query hides the request lifecycle behind hooks and cache tags. That is useful until a cache miss or stale response needs debugging. You must understand providesTags and invalidatesTags to predict refetches. The library also adds bundle size compared with a plain thunk, and it is a poor fit for streaming or socket-based data where there is no request/response pair to cache.
Actionable closing
Start a new data feature by asking whether the source is HTTP and cacheable. If yes, scaffold an createApi slice and verify with Redux DevTools and the Network tab. If the logic is local, event-driven, or non-HTTP, keep it in createSlice with a thunk. Check the store shape in DevTools after the first user action to confirm the data landed under the expected reducer path.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.