MediumRedux Toolkit (RTK) — The Complete Beginner-to-Expert Guide | by Omkesh B. Kendre | Medium
Sitemap
Omkesh B. Kendre

Omkesh B. Kendre

const OMKESH = { code: ["JS", "React", "Node"], passion: "Build. Break. Learn.", writes: "What I discover." };

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.

25 min readAug 20, 2026

--

Press enter or click to view image in full size

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.

Official Redux Toolkit Documentation

📚 What You Will Learn

Press enter or click to view image in full size

1. First: What Problem Does Redux Solve?

Imagine your application has:

App
├── Header
├── Sidebar
├── ProductList
├── Cart
├── Checkout
└── Profile

Multiple components may need the same information:

user
cart
products
permissions
theme
notifications

You could pass everything through props:

App
↓
Header
↓
UserMenu
↓
Profile

This can quickly become prop drilling.

Redux provides a centralized state container:

                Redux Store
│
┌────────────┼────────────┐
↓ ↓ ↓
user cart products
│ │ │
↓ ↓ ↓
Components Components Components

The 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 Query

The 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
│
↓
UI

For 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) => newState

4.5 Dispatch

dispatch() sends an action to Redux.

dispatch(increment());

5. Installing Redux Toolkit

For a React application:

npm install @reduxjs/toolkit react-redux

Redux 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.jsx

Start 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
│
└── users

Your 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()
DevTools

you 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
authSlice

Example:

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/decrement

Reducer

counterSlice.reducer

The 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 state

This 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 value

17. 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-renders

18. 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 userLoggedOut

21. 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:

extraReducers

comes 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_FAILURE

RTK 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
rejected

createAsyncThunk 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: true

consider:

status: "idle"

Possible values:

idle
loading
succeeded
failed

This represents a simple state machine:

idle
↓
loading
↓
succeeded

or

loading
↓
failed

This is usually clearer than multiple booleans such as:

loading
success
error

because 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 request

createAsyncThunk() 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 Toolkit

You 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
↓
reducer

Middleware 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 actions

Example:

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 deduplication

The 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
↓
component

RTK 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
↓
Component

This 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 refetches

This 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 comment

RTK 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 preferences

Redux slices can manage this.

Server state

Examples:

users from API
products
orders
permissions
comments

RTK Query is often a better fit.

Think:

               Application State
│
┌─────────┴─────────┐
↓ ↓
Client State Server State
│ │
createSlice RTK Query

40. 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.data

to:

state.users.entities

If 50 components directly access:

state.users.data

you have a maintenance problem.

With selectors:

selectUsers(state)

you can change the implementation in one place.

42. Memoized Selectors with createSelector

Suppose:

state.users

contains 10,000 users.

You want:

activeUsers

You 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
removeAll

This 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/state

48. 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 effect

Example:

userLoggedIn
↓
listener
↓
fetch permissions
↓
dispatch permissionsLoaded

This 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 sense

Listener 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
null

Be careful with:

Date
Map
Set
Promise
function
class instance
DOM node
File

Why?

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 cache

is often preferable.

For client-owned state:

createSlice

may 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:

modalSlice

for 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
selector

62. Don’t Store Derived State

Suppose:

state = {
items: [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
]
}

Don’t necessarily store:

total: 250

because 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 value

One 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.length

consider 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 after

Example:

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 state

For RTK Query:

mock API
↓
render component
↓
query hook
↓
verify UI

67. 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 = 1

That 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.ts

For RTK Query:

usersApi

contains server communication.

For local application state:

usersSlice

contains 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
commentsApi

RTK 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.js

Endpoints 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 MFE3

Or 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 requests

You 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 requests

73. Automatic Token Refresh

A production API layer may need:

Request
↓
401?
↓
Refresh token
↓
Retry request

RTK 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 request

You 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 → rollback

This can make applications feel significantly faster.

75. Pagination

For large APIs:

GET /products?page=1
GET /products?page=2
GET /products?page=3

RTK 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 middleware

The 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.users

unless there is a clear reason.

You can easily create synchronization problems.

Mistake 4 — Storing derived data

Don’t duplicate:

items
+
total

when total can be calculated.

Mistake 5 — Giant slices

Avoid:

appSlice.js

with:

users
cart
products
auth
orders
notifications
settings

Prefer feature boundaries.

78. Mistake: Creating Too Many Selectors

Selectors are useful, but don’t create abstraction for abstraction’s sake.

Simple:

state => state.auth.user

is 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 free

Use:

RTK Query
createAsyncThunk
listener middleware

for async side effects.

81. How to Choose the Right RTK API

This is one of the most important tables to remember.

Press enter or click to view image in full size

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
↓
createSlice

For side effects:

Async API workflow?
↓
RTK Query or createAsyncThunk


React-independent action-driven workflow?
↓
Listener Middleware


Simple component-local effect?
↓
React useEffect

83. A Production-Style Example

Imagine an e-commerce application.

Redux
│
├── authSlice
│
├── cartSlice
│
├── uiSlice
│
└── API
│
├── products
├── users
├── orders
└── payments

authSlice

Stores:

{
user,
token,
status
}

cartSlice

Stores:

{
items
}

uiSlice

Stores:

{
theme,
sidebarOpen
}

RTK Query

Handles:

products
orders
users
payments

This 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
Provider

Then:

configureStore
createSlice

Level 2 — Intermediate

Learn:

Payload
extraReducers
createAsyncThunk
middleware
selectors
Immer
error handling
loading states

Level 3 — Advanced

Learn:

RTK Query
cache
tags
mutations
optimistic updates
pagination
entity adapter
memoized selectors

Level 4 — Expert

Learn:

listener middleware
advanced TypeScript
custom middleware
performance
architecture
code splitting
lazy reducer injection
testing
authentication
token refresh
large-scale state design

86. The Most Important Concept: Redux Is Not Your Database

This is a very important architectural distinction.

Your backend database might contain:

10 million users

Your frontend does not need:

10 million users

in 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 state

87. 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 architecture

Context 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 support

Zustand 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 Query

is a natural combination.

If you don’t need Redux at all and primarily need server-state management:

TanStack Query

may be worth considering.

90. What Redux Toolkit Actually Solved

Traditional Redux:

Action constants
↓
Action creators
↓
Reducers
↓
Immutable updates
↓
Middleware
↓
Store
↓
DevTools

Modern Redux Toolkit:

 createSlice
↓
configureStore
↓
RTK Query
↓
TypeScript
↓
DevTools

RTK 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
│
↓
UI

92. 🎯 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 Query

before 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 UI

Conclusion

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 Tooling

The 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 Middleware

Once 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.

--

--

Turning code into knowledge 🚀 | JavaScript • React • Node.js • AI | I write deep dives, lessons learned & real-world engineering stories.

See more from me