Redux Toolkit (RTK) — The Complete Beginner-to-Expert Guide
From your first Redux store to production-ready state management, async workflows, RTK Query, TypeScript, performance, architecture, and interview preparation.
If you have ever looked at Redux code and thought:
“Why do I need so many files just to update one piece of state?”
You are not alone.
Traditional Redux often required action types, action creators, reducers, middleware, immutable update logic, store configuration, and plenty of boilerplate.
Redux Toolkit (RTK) was created to solve exactly this problem.
Today, Redux Toolkit is the recommended way to write Redux logic. It provides modern APIs for store configuration, reducers, actions, async workflows, normalized state, selectors, and data fetching.
This guide takes you from absolute beginner → intermediate → advanced → production-ready → interview-ready.
📚 What You Will Learn
1. First: What Problem Does Redux Solve?
Imagine your application has:
App
├── Header
├── Sidebar
├── ProductList
├── Cart
├── Checkout
└── ProfileMultiple components may need the same information:
user
cart
products
permissions
theme
notificationsYou could pass everything through props:
App
↓
Header
↓
UserMenu
↓
ProfileThis can quickly become prop drilling.
Redux provides a centralized state container:
Redux Store
│
┌────────────┼────────────┐
↓ ↓ ↓
user cart products
│ │ │
↓ ↓ ↓
Components Components ComponentsThe important idea is:
Redux is a predictable state container.
But Redux itself is not specifically a React library.
React applications commonly use React-Redux to connect React components to the Redux store.
2. So What Is Redux Toolkit?
Redux Toolkit is the official recommended approach for writing Redux logic.
Instead of manually writing:
const ADD_TODO = "ADD_TODO";
function addTodo(todo) {
return {
type: ADD_TODO,
payload: todo
};
}
function reducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload];
default:
return state;
}
}RTK lets you write:
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo(state, action) {
state.push(action.payload);
}
}
});Much less code.
But RTK is much more than createSlice().
It provides APIs such as:
configureStore
createSlice
createReducer
createAction
createAsyncThunk
createEntityAdapter
createSelector
createListenerMiddleware
combineSlices
RTK QueryThe official API reference currently includes these APIs as core parts of Redux Toolkit.
3. Redux Toolkit Mental Model
Before learning APIs, understand this flow:
User Interaction
│
↓
dispatch()
│
↓
Action
│
↓
Middleware
│
↓
Reducer
│
↓
Redux Store
│
↓
React-Redux
│
↓
Component
│
↓
UIFor example:
dispatch(increment());The action reaches the Redux store.
The reducer processes it.
The state changes.
React-Redux detects the relevant state change.
The component renders the updated value.
4. The Five Redux Concepts You Must Understand
Before becoming an RTK expert, understand these five concepts.
4.1 Store
The store contains your application state.
{
user: {
id: 1,
name: "Omkesh"
},
cart: {
items: []
},
products: []
}4.2 State
State is the data managed by Redux.
{
count: 10
}4.3 Action
An action describes what happened.
{
type: "counter/increment",
payload: 5
}Think:
“Something happened.”
4.4 Reducer
A reducer determines:
“Given the current state and this action, what should the next state be?”
(state, action) => newState4.5 Dispatch
dispatch() sends an action to Redux.
dispatch(increment());5. Installing Redux Toolkit
For a React application:
npm install @reduxjs/toolkit react-reduxRedux Toolkit contains the Redux logic.
React-Redux connects Redux to React.
6. Your First Redux Toolkit Store
Create:
src/
├── app/
│ └── store.js
├── features/
│ └── counter/
│ └── counterSlice.js
└── main.jsxStart with the store:
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {}
});That’s it.
Compare this with traditional Redux store configuration.
configureStore() automatically handles common configuration such as middleware and Redux DevTools and includes useful development checks.
7. configureStore() Explained
This is one of the most important RTK APIs.
const store = configureStore({
reducer: {
counter: counterReducer,
users: usersReducer
}
});Conceptually:
Redux Store
│
├── counter
│
└── usersYour resulting state might look like:
{
counter: {
value: 10
},
users: {
data: [],
status: "idle"
}
}configureStore() can accept an object of slice reducers and automatically combine them into the root reducer.
8. What Does configureStore() Give You?
Among other things:
✅ Middleware
Thunk middleware is included by default.
✅ Redux DevTools
DevTools integration is enabled automatically.
✅ Development checks
RTK provides development middleware that checks for common mistakes such as accidental mutation and non-serializable values.
✅ Easier configuration
Instead of manually configuring:
createStore()
applyMiddleware()
compose()
DevToolsyou usually write:
configureStore({
reducer
});This is one of the biggest reasons RTK dramatically reduces Redux boilerplate.
9. createSlice() — The Heart of Redux Toolkit
If you learn only one RTK API first, learn:
createSlice()A slice represents one logical section of Redux state.
For example:
userSlice
cartSlice
productSlice
notificationSlice
authSliceExample:
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
}
}
});Then:
export const {
increment,
decrement
} = counterSlice.actions;
export default counterSlice.reducer;10. What Does createSlice() Generate?
This:
createSlice({
name: "counter",
reducers: {
increment() {},
decrement() {}
}
});automatically generates:
Action creators
increment()
decrement()Action types
counter/increment
counter/decrementReducer
counterSlice.reducerThe generated action type is based on the slice name and reducer name.
So instead of manually creating:
const INCREMENT = "counter/increment";RTK does it for you.
11. Why Does RTK Allow “Mutation”?
You might see:
increment(state) {
state.value += 1;
}At first glance, this looks like Redux’s biggest rule has been broken.
Traditional Redux says:
Don’t mutate state.
RTK uses Immer internally.
Immer lets you write mutation-like code while producing an immutable state update.
So:
state.value += 1;is effectively translated into an immutable update.
Conceptually:
Your code
↓
Immer draft
↓
Immutable next stateThis is especially useful for deeply nested state.
12. Important Immer Rule
This is valid:
state.user.name = "John";But don’t do this:
function reducer(state) {
state.value = 10;
return {};
}You generally choose one approach:
Mutate the Immer draft
state.value = 10;OR
Return a completely new state
return {
value: 10
};Do not mix both in the same case reducer.
13. Complete Counter Example
counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
incrementByAmount(state, action) {
state.value += action.payload;
},
reset(state) {
state.value = 0;
}
}
});
export const {
increment,
decrement,
incrementByAmount,
reset
} = counterSlice.actions;
export default counterSlice.reducer;14. Add the Slice to the Store
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer
}
});Now your Redux state is:
{
counter: {
value: 0
}
}15. Connect Redux to React
Wrap your application with <Provider>.
import { Provider } from "react-redux";
import { store } from "./app/store";
<Provider store={store}>
<App />
</Provider>;Now React components can access Redux.
16. Reading Redux State with useSelector
import { useSelector } from "react-redux";
function Counter() {
const count = useSelector(
state => state.counter.value
);
return <h1>{count}</h1>;
}Think:
useSelector()
↓
Read Redux state
↓
Component receives value17. Updating Redux State with useDispatch
import { useDispatch } from "react-redux";
function Counter() {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Increment
</button>
);
}The complete cycle becomes:
Button click
↓
dispatch(increment())
↓
counter/increment
↓
counter reducer
↓
state.value += 1
↓
React component re-renders18. Payload — Passing Data to Reducers
Suppose you want:
incrementByAmount(10)Dispatch:
dispatch(incrementByAmount(10));The action becomes approximately:
{
type: "counter/incrementByAmount",
payload: 10
}Reducer:
incrementByAmount(state, action) {
state.value += action.payload;
}19. A Better Payload Pattern
For complex data:
dispatch(
userUpdated({
id: 10,
name: "John",
role: "admin"
})
);Reducer:
userUpdated(state, action) {
const { id, name, role } = action.payload;
state.users[id] = {
id,
name,
role
};
}A common best practice is to keep payloads clear and predictable.
20. createAction()
createSlice() is usually what you want.
But RTK also provides:
createAction()Example:
import { createAction } from "@reduxjs/toolkit";
const userLoggedOut = createAction(
"auth/userLoggedOut"
);Then:
dispatch(userLoggedOut());This is useful when an action needs to be shared across multiple slices.
For example:
auth
└── userLoggedOut
cart
└── responds to userLoggedOut
notifications
└── responds to userLoggedOut
products
└── responds to userLoggedOut21. extraReducers() — One of the Most Important Concepts
Reducers inside:
reducers: {}usually define actions owned by that slice.
But sometimes a slice needs to respond to an action created somewhere else.
That’s where:
extraReducerscomes in.
Example:
const cartSlice = createSlice({
name: "cart",
initialState: {
items: []
},
reducers: {
itemAdded(state, action) {
state.items.push(action.payload);
}
},
extraReducers: builder => {
builder.addCase(userLoggedOut, state => {
state.items = [];
});
}
});Now the cart reacts to:
userLoggedOut()even though the action belongs conceptually to authentication.
This is also commonly used with createAsyncThunk().
22. createAsyncThunk() — Handling Async Operations
Traditional Redux often required three actions:
FETCH_USERS_REQUEST
FETCH_USERS_SUCCESS
FETCH_USERS_FAILURERTK provides:
createAsyncThunk()Example:
export const fetchUsers = createAsyncThunk(
"users/fetchUsers",
async () => {
const response = await fetch(
"https://example.com/users"
);
return response.json();
}
);RTK automatically creates lifecycle actions:
pending
fulfilled
rejectedcreateAsyncThunk is specifically designed to generate promise lifecycle actions around an async function.
23. Handling createAsyncThunk() in a Slice
const usersSlice = createSlice({
name: "users",
initialState: {
data: [],
status: "idle",
error: null
},
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchUsers.pending, state => {
state.status = "loading";
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = "succeeded";
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
}
});This is one of the most common RTK patterns.
24. Why Use a Status Field?
Instead of:
loading: trueconsider:
status: "idle"Possible values:
idle
loading
succeeded
failedThis represents a simple state machine:
idle
↓
loading
↓
succeeded
or
loading
↓
failedThis is usually clearer than multiple booleans such as:
loading
success
errorbecause boolean combinations can become contradictory.
25. Handling Errors Correctly
You can customize errors:
export const fetchUser = createAsyncThunk(
"users/fetchUser",
async (userId, thunkAPI) => {
try {
const response = await fetch(`/users/${userId}`);
if (!response.ok) {
throw new Error("Unable to fetch user");
}
return await response.json();
} catch (error) {
return thunkAPI.rejectWithValue(
"User could not be loaded"
);
}
}
);Then:
.addCase(fetchUser.rejected, (state, action) => {
state.error = action.payload;
});rejectWithValue() is especially useful when you want a known server/application error payload rather than only the generic serialized error.
26. dispatch(thunk()).unwrap()
A very useful pattern:
try {
const user = await dispatch(
fetchUser(10)
).unwrap();
console.log(user);
} catch (error) {
console.error(error);
}unwrap() allows the calling code to receive the fulfilled payload or throw the rejected value.
This is useful when a component needs to perform another action only after an async operation succeeds.
27. Conditional Requests with condition
Sometimes you don’t want to send duplicate requests.
For example:
Users already loading?
↓
YES
↓
Don't send another requestcreateAsyncThunk() supports a condition option for deciding whether the thunk should execute.
Conceptually:
createAsyncThunk(
"users/fetchUsers",
async () => {
// API call
},
{
condition: (_, { getState }) => {
const status = getState().users.status;
return status === "idle";
}
}
);28. Abort / Cancellation
Async requests can also need cancellation.
For example:
Search:
React
↓
React Redux
↓
React Redux ToolkitYou don’t necessarily want every old request completing.
createAsyncThunk() exposes cancellation-related APIs through its thunk API and supports an abort signal.
This becomes especially important for:
- Search
- Autocomplete
- Rapid navigation
- Large API requests
29. Middleware
Middleware sits between:
dispatch()
↓
middleware
↓
reducerMiddleware can:
- log actions
- perform async work
- react to actions
- modify workflows
- trigger side effects
RTK automatically includes thunk middleware in configureStore().
30. When Should You Use a Thunk?
A thunk is useful when you need:
API call
↓
read state
↓
perform logic
↓
dispatch actionsExample:
const saveUser = user => async (
dispatch,
getState
) => {
const token = getState().auth.token;
const response = await api.saveUser(
user,
token
);
dispatch(userSaved(response));
};31. RTK Query — The Big One
Now we reach one of the most important parts of modern Redux Toolkit:
RTK Query
RTK Query is a data fetching and caching solution built into Redux Toolkit.
It can eliminate the need to manually write:
loading state
error state
fetch thunk
success reducer
failure reducer
cache logic
refetch logic
request deduplicationThe official documentation describes RTK Query as a purpose-built data fetching and caching solution for Redux applications.
32. Traditional API Approach
Without RTK Query, you might write:
component
↓
dispatch(fetchUsers())
↓
pending
↓
API
↓
fulfilled/rejected
↓
reducer
↓
store
↓
componentRTK Query simplifies this.
33. Basic RTK Query API
import {
createApi,
fetchBaseQuery
} from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "/api"
}),
endpoints: builder => ({
getUsers: builder.query({
query: () => "/users"
})
})
});
export const {
useGetUsersQuery
} = api;Component:
function Users() {
const {
data,
isLoading,
error
} = useGetUsersQuery();
if (isLoading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Something went wrong</p>;
}
return (
<ul>
{data?.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}34. RTK Query’s Mental Model
Think:
Component
│
↓
useGetUsersQuery()
│
↓
RTK Query cache
│
├── Cached data?
│ ↓
│ Use it
│
└── No data?
↓
API call
↓
Cache
↓
ComponentThis is fundamentally different from manually storing API results in a slice.
35. Query vs Mutation
RTK Query generally separates operations into:
Query
Used for reading data:
getUsers: builder.query({
query: () => "/users"
})Mutation
Used for changing server data:
addUser: builder.mutation({
query: user => ({
url: "/users",
method: "POST",
body: user
})
})36. Mutation Example
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: "/api"
}),
endpoints: builder => ({
addUser: builder.mutation({
query: user => ({
url: "/users",
method: "POST",
body: user
})
})
})
});Component:
const [addUser, { isLoading }] =
useAddUserMutation();
const handleSubmit = async () => {
await addUser({
name: "Omkesh"
});
};37. RTK Query Cache Tags
One of RTK Query’s most powerful features is automated cache invalidation.
Example:
getUsers: builder.query({
query: () => "/users",
providesTags: ["Users"]
}),
addUser: builder.mutation({
query: user => ({
url: "/users",
method: "POST",
body: user
}),
invalidatesTags: ["Users"]
})Conceptually:
getUsers
↓
Cache Users
addUser
↓
Users changed
↓
invalidate Users cache
↓
getUsers refetchesThis can remove a large amount of manual synchronization code.
38. Why RTK Query Is Often Better Than createAsyncThunk
For simple API data:
GET users
GET products
POST order
PUT profile
DELETE commentRTK Query is often the better choice.
Use createAsyncThunk() when you need custom asynchronous workflows that aren't primarily server-cache management.
A useful rule:
Server data?
↓
Try RTK Query first.
Complex application workflow?
↓
Consider createAsyncThunk or listener middleware.39. Client State vs Server State
This distinction is extremely important.
Client state
Examples:
modalOpen
selectedTab
theme
wizardStep
local preferencesRedux slices can manage this.
Server state
Examples:
users from API
products
orders
permissions
commentsRTK Query is often a better fit.
Think:
Application State
│
┌─────────┴─────────┐
↓ ↓
Client State Server State
│ │
createSlice RTK Query40. Selectors
Instead of allowing components to understand your entire Redux state shape:
const count = useSelector(
state => state.counter.value
);you can create selectors:
const selectCount = state =>
state.counter.value;Then:
const count = useSelector(selectCount);Selectors provide a clean abstraction around state access.
41. Why Selectors Matter
Imagine your state changes from:
state.users.datato:
state.users.entitiesIf 50 components directly access:
state.users.datayou have a maintenance problem.
With selectors:
selectUsers(state)you can change the implementation in one place.
42. Memoized Selectors with createSelector
Suppose:
state.userscontains 10,000 users.
You want:
activeUsersYou could filter every time:
const activeUsers = users.filter(
user => user.active
);A memoized selector can avoid unnecessary recalculation when its inputs haven’t changed.
import { createSelector } from "@reduxjs/toolkit";
const selectUsers = state =>
state.users.data;
const selectActiveUsers = createSelector(
[selectUsers],
users =>
users.filter(user => user.active)
);createSelector() is based on memoized selector logic and is included in Redux Toolkit.
43. A Common Selector Mistake
Avoid doing this unnecessarily:
const users = useSelector(
state => state.users.map(user => ({
...user
}))
);You’re creating a new array/object every time the selector runs.
Prefer:
const users = useSelector(
selectUsers
);and derive data through memoized selectors when needed.
44. createEntityAdapter()
Large collections can become difficult to manage.
Imagine:
[
{ id: 1, name: "A" },
{ id: 2, name: "B" },
{ id: 3, name: "C" }
]Finding/updating individual entities repeatedly can become inefficient and verbose.
RTK provides:
createEntityAdapter()It standardizes normalized state into:
{
ids: [1, 2, 3],
entities: {
1: { id: 1, name: "A" },
2: { id: 2, name: "B" },
3: { id: 3, name: "C" }
}
}It also provides prebuilt CRUD reducers and selectors.
45. Entity Adapter Example
import {
createEntityAdapter,
createSlice
} from "@reduxjs/toolkit";
const usersAdapter =
createEntityAdapter();
const usersSlice = createSlice({
name: "users",
initialState:
usersAdapter.getInitialState(),
reducers: {
userAdded:
usersAdapter.addOne,
usersReceived:
usersAdapter.setAll,
userUpdated:
usersAdapter.updateOne,
userRemoved:
usersAdapter.removeOne
}
});Now you get operations such as:
addOne
addMany
setOne
setMany
setAll
updateOne
updateMany
upsertOne
upsertMany
removeOne
removeMany
removeAllThis is extremely useful for normalized client state.
46. When Should You Use Entity Adapter?
Use it when:
- you have large collections
- entities are frequently updated individually
- entities have stable IDs
- multiple parts of the application reference the same entities
- normalized state makes your architecture clearer
Don’t use it just because it exists.
For a simple:
const settings = {
theme: "dark"
};there is no reason to introduce an entity adapter.
47. Listener Middleware
RTK also provides:
createListenerMiddleware()It allows you to run logic in response to actions or state changes.
The official docs describe it as a lightweight alternative to more complex middleware approaches such as sagas and observables.
Think of it like:
React useEffect
↓
reacts to component state/props
Redux Listener Middleware
↓
reacts to Redux actions/state48. Listener Middleware Example
import {
createListenerMiddleware
} from "@reduxjs/toolkit";
const listenerMiddleware =
createListenerMiddleware();
listenerMiddleware.startListening({
actionCreator: userLoggedIn,
effect: async (action, listenerApi) => {
console.log(
"User logged in:",
action.payload
);
}
});Then add it to the store:
configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(
listenerMiddleware.middleware
)
});The official documentation recommends placing listener middleware before the serializability check because listener registration actions can contain functions.
49. Listener Middleware vs Thunk
Thunk
Usually:
Component
↓
dispatch(asyncOperation())The component intentionally starts the workflow.
Listener
Usually:
Action happens
↓
Listener notices it
↓
Side effectExample:
userLoggedIn
↓
listener
↓
fetch permissions
↓
dispatch permissionsLoadedThis can keep components much cleaner.
50. Listener Middleware vs Saga
For many applications:
Simple async logic
↓
Thunk
Complex action-driven workflows
↓
Listener Middleware
Highly specialized existing Saga architecture
↓
Saga may still make senseListener middleware provides async primitives such as take, condition, pause, fork, and cancellation-related capabilities.
51. createReducer()
Although createSlice() is more common, RTK also provides:
createReducer()Example:
const counterReducer = createReducer(
{ value: 0 },
builder => {
builder
.addCase(increment, state => {
state.value++;
})
.addCase(decrement, state => {
state.value--;
});
}
);Use it when you need reducer logic without necessarily creating a slice.
For most feature-based application code:
createSlice()will usually be the better default.
52. Matching Utilities
RTK provides matching utilities that are useful for handling groups of actions.
For example:
builder.addMatcher(
action => action.type.endsWith("/rejected"),
(state, action) => {
state.error = action.error;
}
);You can also use utilities such as:
isAnyOf()
isAllOf()
isPending()
isFulfilled()
isRejected()
isRejectedWithValue()These become especially useful in larger applications where multiple async operations share common behavior.
53. Modern RTK Architecture
A scalable application can look like:
src/
│
├── app/
│ ├── store.ts
│ ├── hooks.ts
│ └── listenerMiddleware.ts
│
├── features/
│ │
│ ├── auth/
│ │ ├── authSlice.ts
│ │ ├── authSelectors.ts
│ │ └── authApi.ts
│ │
│ ├── users/
│ │ ├── usersSlice.ts
│ │ ├── usersSelectors.ts
│ │ └── usersApi.ts
│ │
│ └── cart/
│ ├── cartSlice.ts
│ └── cartSelectors.ts
│
└── components/A good rule is:
Organize code around features, not Redux concepts.
Avoid:
actions/
reducers/
constants/
selectors/
api/for the entire application.
Get Omkesh B. Kendre’s stories in your inbox
Join Medium for free to get updates from this writer.
Prefer:
features/
users/
cart/
auth/This keeps related code together.
54. TypeScript with Redux Toolkit
RTK is written in TypeScript and provides strong TypeScript integration.
Typical setup:
import {
configureStore
} from "@reduxjs/toolkit";
import counterReducer
from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
export type RootState =
ReturnType<typeof store.getState>;
export type AppDispatch =
typeof store.dispatch;This is extremely important.
55. Typed Hooks
Instead of repeatedly writing:
useDispatch<AppDispatch>()create typed hooks.
Modern React-Redux versions support:
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>();
export const useAppSelector =
useSelector.withTypes<RootState>();The official RTK TypeScript documentation recommends extracting the store-derived RootState and AppDispatch types and provides typed-hook patterns.
Then components can simply use:
const dispatch = useAppDispatch();
const user = useAppSelector(
state => state.auth.user
);56. Type Your Payloads
Instead of:
incrementByAmount(state, action) {
// ...
}Type it:
import type {
PayloadAction
} from "@reduxjs/toolkit";
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
incrementByAmount(
state,
action: PayloadAction<number>
) {
state.value += action.payload;
}
}
});Now TypeScript knows:
dispatch(
incrementByAmount(10)
);is valid.
But:
dispatch(
incrementByAmount("10")
);is invalid.
57. TypeScript: Don’t Over-Type Everything
RTK has strong inference.
You don’t need to manually type every single thing.
Prefer:
const store = configureStore({
reducer: {
users: usersReducer
}
});
type RootState =
ReturnType<typeof store.getState>;instead of manually maintaining:
interface RootState {
users: UsersState;
...
}The store can become the source of truth.
58. Serializability
Redux state and actions should generally contain serializable values.
Good:
string
number
boolean
array
plain object
nullBe careful with:
Date
Map
Set
Promise
function
class instance
DOM node
FileWhy?
Redux DevTools, persistence, debugging, replayability, and predictable state transitions all work best with serializable data.
RTK includes development middleware to detect common non-serializable values.
59. Should You Put API Response Objects Directly in Redux?
It depends.
For server data managed by RTK Query:
RTK Query cacheis often preferable.
For client-owned state:
createSlicemay make sense.
The important question is not:
“Can I put this in Redux?”
Almost anything can be represented.
The better question is:
“Does this data need to live in global Redux state?”
60. What Should NOT Go Into Redux?
Don’t automatically put every piece of state into Redux.
For example:
const [isOpen, setIsOpen] =
useState(false);is usually perfectly fine.
You don’t need:
modalSlicefor every modal.
Use local React state when:
- only one component needs it
- it is temporary UI state
- there is no need for global access
Use Redux when state needs broader coordination or persistence across component boundaries.
61. Redux State Design Principle
Ask:
Question 1
Who needs this state?
Question 2
Does it need to survive component unmounting?
Question 3
Does multiple unrelated UI need it?
Question 4
Is it server data?
Question 5
Can it simply be derived?
That determines whether you need:
useState
useReducer
Redux slice
RTK Query
selector62. Don’t Store Derived State
Suppose:
state = {
items: [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
]
}Don’t necessarily store:
total: 250because total can be derived.
Use a selector:
const selectCartTotal = createSelector(
[selectCartItems],
items =>
items.reduce(
(total, item) =>
total +
item.price * item.quantity,
0
)
);Now:
Source state
↓
Selector
↓
Derived valueOne source of truth.
63. Redux Performance
Redux performance problems commonly come from:
- selecting too much state
- creating new objects unnecessarily
- expensive selectors
- huge components subscribing to broad state
- storing unnecessary data
- unnecessary updates
Bad:
const entireState = useSelector(
state => state
);Better:
const user = useSelector(
state => state.auth.user
);Even better:
const userName = useSelector(
selectUserName
);64. Don’t Select More Than You Need
Instead of:
const users = useSelector(
state => state.users
);when the component only needs:
users.lengthconsider selecting a focused value:
const userCount = useSelector(
selectUserCount
);This can reduce unnecessary component updates.
65. Redux DevTools
Redux DevTools is one of Redux’s biggest strengths.
You can inspect:
Action
State before
Action payload
State afterExample:
counter/increment
Prev State:
{ value: 0 }
Action:
{ type: "counter/increment" }
Next State:
{ value: 1 }This makes debugging state transitions dramatically easier.
configureStore() enables DevTools integration automatically.
66. Testing Redux Toolkit
You should test behavior rather than implementation details.
For reducers:
const state = {
value: 0
};
const result =
counterReducer(
state,
increment()
);
expect(result.value).toBe(1);For async logic:
dispatch thunk
↓
mock API
↓
fulfilled/rejected
↓
verify stateFor RTK Query:
mock API
↓
render component
↓
query hook
↓
verify UI67. Don’t Test Immer
Don’t write tests asking:
“Did Immer create the immutable object exactly this way?”
Test behavior:
Before:
count = 0
Action:
increment
After:
count = 1That is the contract your application cares about.
68. API Layer Architecture
For a large application, a useful pattern is:
features/
users/
usersApi.ts
usersSlice.ts
usersSelectors.tsFor RTK Query:
usersApicontains server communication.
For local application state:
usersSlicecontains client-owned state.
This separation makes responsibilities clearer.
69. One API Slice vs Many API Slices
A common mistake is creating an API slice for every endpoint:
usersApi
productsApi
ordersApi
commentsApiRTK Query generally benefits from organizing endpoints into an appropriate API slice because the API slice manages cache state and middleware.
A common architecture is:
api/
baseApi.js
features/
users/
usersApi.js
products/
productsApi.jsEndpoints can be injected into a shared API instance when appropriate.
70. combineSlices()
Modern Redux Toolkit also includes:
combineSlices()This is useful for combining slices and supports more advanced patterns such as lazy-loaded reducer injection. The current RTK API reference lists it alongside the core reducer APIs.
This becomes interesting in:
- large applications
- micro-frontends
- code splitting
- lazy-loaded features
For a beginner application, however, standard configureStore({ reducer: {...} }) is enough.
71. Redux Toolkit in Micro-Frontends
For micro-frontends, you may need to think carefully about:
Who owns the store?
Who owns each slice?
How are actions shared?
How are reducers injected?
How is state isolated?Possible architecture:
Shell Application
│
↓
Redux Store
│
┌─────┼─────┐
↓ ↓ ↓
MFE1 MFE2 MFE3Or each micro-frontend can own isolated state.
There is no universal answer.
The key is to avoid turning Redux into a hidden dependency between independently deployable applications.
72. RTK Query and Authentication
A common production architecture:
authSlice
│
├── accessToken
├── user
└── authentication state
baseApi
│
└── API requestsYou can customize baseQuery to attach authentication information.
For example:
const baseQuery =
fetchBaseQuery({
baseUrl: "/api",
prepareHeaders: (
headers,
{ getState }
) => {
const token =
getState().auth.token;
if (token) {
headers.set(
"authorization",
`Bearer ${token}`
);
}
return headers;
}
});This creates a clean separation:
Auth state
↓
API layer
↓
HTTP requests73. Automatic Token Refresh
A production API layer may need:
Request
↓
401?
↓
Refresh token
↓
Retry requestRTK Query allows custom baseQuery implementations, so you can centralize this behavior rather than duplicating it in every endpoint.
Conceptually:
const baseQueryWithReauth =
async (args, api, extraOptions) => {
let result =
await baseQuery(
args,
api,
extraOptions
);
if (result.error?.status === 401) {
// refresh token
result =
await baseQuery(
args,
api,
extraOptions
);
}
return result;
};This is a common advanced RTK Query pattern.
74. Optimistic Updates
Suppose:
User clicks Like
↓
UI immediately shows ❤️
↓
API requestYou don’t necessarily want the user waiting for the server.
RTK Query supports optimistic update patterns using cache utilities.
Conceptually:
Current cache
↓
Optimistically modify
↓
API request
↓
Success → keep
Failure → rollbackThis can make applications feel significantly faster.
75. Pagination
For large APIs:
GET /products?page=1
GET /products?page=2
GET /products?page=3RTK Query can manage query arguments and caching.
Example:
getProducts: builder.query({
query: page =>
`/products?page=${page}`
});Component:
const {
data,
isFetching
} = useGetProductsQuery(page);The cache is keyed according to the query arguments.
76. Search APIs
Consider:
Search: "rea"
Search: "reac"
Search: "react"Without careful design, you can send many requests.
Possible strategies include:
debounce
abort/cancel
cache
query arguments
listener middlewareThe best solution depends on the UX and API behavior.
77. Common RTK Mistakes
Mistake 1 — Putting everything in Redux
Don’t.
Use local state when appropriate.
Mistake 2 — Using createAsyncThunk for every API
If the problem is primarily server data fetching and caching, consider RTK Query first.
Mistake 3 — Duplicating server data
Avoid:
RTK Query cache
+
usersSlice.usersunless there is a clear reason.
You can easily create synchronization problems.
Mistake 4 — Storing derived data
Don’t duplicate:
items
+
totalwhen total can be calculated.
Mistake 5 — Giant slices
Avoid:
appSlice.jswith:
users
cart
products
auth
orders
notifications
settingsPrefer feature boundaries.
78. Mistake: Creating Too Many Selectors
Selectors are useful, but don’t create abstraction for abstraction’s sake.
Simple:
state => state.auth.useris fine.
Use reusable named selectors when:
- multiple components need the same logic
- state structure is complex
- derived data is expensive
- you want a stable abstraction
79. Mistake: Putting Functions in Redux State
Avoid:
{
onClick: () => {}
}Redux state should generally remain serializable.
Instead store:
{
selectedUserId: 10
}and let the component decide what function to execute.
80. Mistake: Calling APIs Inside Reducers
Never do:
reducers: {
loadUsers(state) {
fetch("/users");
}
}Reducers should be:
predictable
synchronous
side-effect freeUse:
RTK Query
createAsyncThunk
listener middlewarefor async side effects.
81. How to Choose the Right RTK API
This is one of the most important tables to remember.
82. The Modern Redux Decision Tree
When you need state:
Do I need global state?
│
NO
↓
useState / useReducer
YES
↓
Is it server data?
│
YES
↓
RTK Query
NO
↓
Is it derived data?
│
YES
↓
Selector
NO
↓
createSliceFor side effects:
Async API workflow?
↓
RTK Query or createAsyncThunk
React-independent action-driven workflow?
↓
Listener Middleware
Simple component-local effect?
↓
React useEffect83. A Production-Style Example
Imagine an e-commerce application.
Redux
│
├── authSlice
│
├── cartSlice
│
├── uiSlice
│
└── API
│
├── products
├── users
├── orders
└── paymentsauthSlice
Stores:
{
user,
token,
status
}cartSlice
Stores:
{
items
}uiSlice
Stores:
{
theme,
sidebarOpen
}RTK Query
Handles:
products
orders
users
paymentsThis separation is much cleaner than putting everything into one giant Redux state tree.
84. Recommended Project Structure
src/
│
├── app/
│ ├── store.ts
│ ├── hooks.ts
│ └── listenerMiddleware.ts
│
├── services/
│ └── api.ts
│
├── features/
│ │
│ ├── auth/
│ │ ├── authSlice.ts
│ │ ├── authSelectors.ts
│ │ └── authApi.ts
│ │
│ ├── cart/
│ │ ├── cartSlice.ts
│ │ └── cartSelectors.ts
│ │
│ └── products/
│ └── productsApi.ts
│
├── components/
│
└── pages/The exact structure can vary.
The principle matters more:
Keep state logic close to the feature that owns it.
85. Beginner → Expert Learning Path
Level 1 — Beginner
Learn:
Redux concept
Store
State
Action
Reducer
Dispatch
Selector
ProviderThen:
configureStore
createSliceLevel 2 — Intermediate
Learn:
Payload
extraReducers
createAsyncThunk
middleware
selectors
Immer
error handling
loading statesLevel 3 — Advanced
Learn:
RTK Query
cache
tags
mutations
optimistic updates
pagination
entity adapter
memoized selectorsLevel 4 — Expert
Learn:
listener middleware
advanced TypeScript
custom middleware
performance
architecture
code splitting
lazy reducer injection
testing
authentication
token refresh
large-scale state design86. The Most Important Concept: Redux Is Not Your Database
This is a very important architectural distinction.
Your backend database might contain:
10 million usersYour frontend does not need:
10 million usersin Redux.
Redux is a client-side state container.
RTK Query’s cache is also not a replacement for your backend database.
Think:
Backend
↓
Source of truth
Frontend
↓
UI state + cached server state87. Redux vs Context API
A common interview question:
“Why use Redux instead of Context?”
Context is primarily a mechanism for passing values through the React tree.
Redux provides:
centralized state management
predictable updates
actions
reducers
middleware
DevTools
selectors
async workflows
RTK Query
structured architectureContext can absolutely be the right solution for some problems.
Don’t choose Redux simply because the application is large.
Choose it when the state-management requirements justify it.
88. Redux vs Zustand
Both can be excellent.
Redux Toolkit provides:
strict predictable architecture
actions/reducers
DevTools
middleware
RTK Query
large ecosystem
strong TypeScript supportZustand generally offers a simpler API.
The choice depends on application requirements and team preferences.
The important lesson:
State management is an architectural decision, not a popularity contest.
89. Redux vs React Query
RTK Query and TanStack Query solve similar server-state problems.
If your application already uses Redux heavily:
Redux + RTK Queryis a natural combination.
If you don’t need Redux at all and primarily need server-state management:
TanStack Querymay be worth considering.
90. What Redux Toolkit Actually Solved
Traditional Redux:
Action constants
↓
Action creators
↓
Reducers
↓
Immutable updates
↓
Middleware
↓
Store
↓
DevToolsModern Redux Toolkit:
createSlice
↓
configureStore
↓
RTK Query
↓
TypeScript
↓
DevToolsRTK didn’t fundamentally change Redux’s principles.
It made the recommended way of using Redux dramatically simpler.
91. A Complete Mental Model
If you remember nothing else, remember this:
Redux Toolkit
│
┌─────────────────┼─────────────────┐
↓ ↓ ↓
Application Async Logic Server Data
State │ │
│ │ │
↓ ↓ ↓
createSlice createAsyncThunk RTK Query
│ │ │
└─────────────────┼─────────────────┘
↓
Store
│
↓
React-Redux
│
↓
UI92. 🎯 Redux Toolkit Interview Cheat Sheet
93. 🔥 Top 15 Interview Questions You Should Be Able to Explain
1. Why was Redux Toolkit created?
To simplify common Redux development and eliminate repetitive boilerplate.
2. What happens when you call dispatch()?
The action passes through middleware and reaches reducers, which calculate the next state.
3. Why does RTK allow state mutation?
Because Immer converts draft mutations into immutable updates.
4. Difference between reducers and extraReducers?
reducers defines the slice's own generated actions.
extraReducers responds to external actions.
5. Difference between createAsyncThunk and RTK Query?
createAsyncThunk handles general async workflows.
RTK Query specializes in server data fetching, caching, synchronization, and invalidation.
6. What are pending, fulfilled, and rejected?
The lifecycle actions generated by createAsyncThunk.
7. What is normalized state?
Representing entities using IDs and lookup maps rather than deeply nested duplicated arrays.
8. Why use createEntityAdapter?
It provides standardized normalized state, CRUD reducers, and selectors.
9. What is a selector?
A function that reads or derives data from Redux state.
10. Why memoize selectors?
To avoid unnecessary expensive recalculations.
11. What is listener middleware?
A lightweight action/state-driven side-effect system.
12. Should Redux contain every UI state?
No.
13. Why should Redux state be serializable?
For predictable debugging, DevTools, persistence, and state replay.
14. Why use feature-based architecture?
It keeps related state logic together and scales better.
15. What is the modern Redux recommendation?
Use Redux Toolkit rather than manually writing traditional Redux patterns.
94. Final Practical Rules
If you’re building a React application today, these rules will take you a long way:
Rule 1
Use:
configureStore()for store setup.
Rule 2
Use:
createSlice()for most client-side Redux state.
Rule 3
Use Immer-style updates inside RTK reducers.
Rule 4
Use:
createAsyncThunk()for custom async workflows when RTK Query isn’t the right fit.
Rule 5
For server data, strongly consider:
RTK Querybefore writing your own loading/error/cache reducers.
Rule 6
Use selectors to encapsulate state access.
Rule 7
Use:
createSelector()for expensive derived data.
Rule 8
Use:
createEntityAdapter()when normalized entity management provides real value.
Rule 9
Use listener middleware for action-driven workflows and side effects when appropriate.
Rule 10
Don’t put everything into Redux.
95. The One Diagram to Remember
USER
│
↓
React Component
│
┌────────┴────────┐
│ │
useSelector dispatch()
│ │
↓ ↓
Redux Store ← Middleware
│ │
↓ ↓
State Async Logic
│
┌───────────┴───────────┐
↓ ↓
createAsyncThunk RTK Query
│ │
↓ ↓
Reducers Cache
│ │
└───────────┬───────────┘
↓
Redux State
│
↓
React UIConclusion
Redux Toolkit is not simply a shorter version of Redux.
It provides a complete modern ecosystem for managing:
Application State
+
Async Workflows
+
Server Data
+
Caching
+
Normalized State
+
Selectors
+
Middleware
+
TypeScript
+
Developer ToolingThe most important APIs to master are:
configureStore()
createSlice()
createAsyncThunk()
createSelector()
createEntityAdapter()
createListenerMiddleware()And for API-heavy applications:
createApi()through RTK Query.
If you’re learning Redux for real-world development, don’t try to memorize every API.
Instead, master the decision:
Local UI state?
→ React state
Global client state?
→ createSlice
Server state?
→ RTK Query
Custom async workflow?
→ createAsyncThunk
Derived state?
→ Selector
Expensive derived state?
→ createSelector
Large normalized collections?
→ createEntityAdapter
Action-driven side effects?
→ Listener MiddlewareOnce this mental model becomes natural, Redux Toolkit stops feeling complicated and starts becoming a very predictable tool for building large React applications.
The goal isn’t to use Redux everywhere.
The goal is to use the right RTK tool for the right state-management problem.









