🧰 Redux Toolkit Implementation
Classic Redux taught you the concepts — and made you type a lot to use them. Redux Toolkit (RTK) is the official, batteries-included answer: it keeps every principle intact while cutting the boilerplate dramatically. In this lesson you'll rebuild the same features with a fraction of the code.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the pain points of hand-written Redux that RTK was designed to solve
- Set up a store with
configureStoreand its sensible defaults - Generate actions and reducers together with
createSlice - Write "mutating" reducer logic safely thanks to Immer
- Connect a slice to React with
Provider,useSelector, anduseDispatch - Organize code by feature and use
createSelectorandcreateEntityAdapterappropriately
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Convert the classic-Redux todo store into a single RTK slice.
In This Lesson
Why Redux Toolkit Exists
Classic Redux is powerful, but the community consistently hit the same friction:
- Boilerplate — separate files for action types, action creators, reducers, and the store, all for one feature.
- Configuration complexity — wiring middleware, DevTools, and enhancers by hand.
- Fragile immutability — manual spreading (
{ ...state }) is verbose and easy to get subtly wrong. - No standard — every codebase invented its own folder structure and conventions.
💡 An analogy: Writing Redux by hand is like making espresso from raw beans — grind, tamp, measure, time the pull. Redux Toolkit is the good automatic machine: press one button and get the same espresso, with the fiddly parts handled correctly for you.
📖 What RTK is
Redux Toolkit is the official, opinionated, "batteries-included" toolset for Redux. It is now the recommended way to write Redux — the docs steer everyone here, and the classic createStore is deprecated. RTK doesn't replace Redux's ideas; it packages them well.
Out of the box RTK gives you a small set of focused APIs:
Installation & configureStore
Install
# npm
npm install @reduxjs/toolkit react-redux
# yarn
yarn add @reduxjs/toolkit react-redux
configureStore
configureStore wraps the old createStore with good defaults. In one call it combines your slice reducers into a root reducer, adds the thunk middleware, turns on the Redux DevTools Extension, and installs development checks that warn you about accidental mutations or non-serializable values.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todosReducer from '../features/todos/todosSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer
}
// devTools, thunk, and dev-only checks are enabled automatically
});
export default store;
💡 Compare that to classic setup
No combineReducers, no manual applyMiddleware, no window.__REDUX_DEVTOOLS_EXTENSION__ dance. The same store that took a dozen fiddly lines is now four obvious ones.
createSlice: The Heart of RTK
createSlice is the API you'll use most. From a single configuration object it generates the reducer, the action creators, and the action type strings — everything a feature needs in one place.
Anatomy of a slice
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter', // namespaces the action types
initialState: { value: 0 },
reducers: {
// key -> action type 'counter/increment'; value -> case reducer
increment: (state) => {
state.value += 1; // looks like mutation — Immer makes it safe
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
}
}
});
// Action creators are generated for you
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// So is the reducer
export default counterSlice.reducer;
That single call produced a reducer, four action creators, and four namespaced action types (counter/increment, and so on). The equivalent classic code would span multiple files and be several times longer.
Prepare callbacks for richer payloads
When an action needs to shape or generate part of its payload — an id, a timestamp — use the { reducer, prepare } form. The prepare function builds the payload; the reducer just applies it. This keeps impure work (like Date.now()) out of the reducer.
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer: (state, action) => {
state.push(action.payload);
},
prepare: (text) => ({
payload: {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date().toISOString()
}
})
},
toggleTodo: (state, action) => {
const todo = state.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
}
}
});
Immer & "Mutating" Reducers
The line state.value += 1 would be a bug in classic Redux. In RTK it's correct — and this is the single biggest quality-of-life win. RTK runs your reducer bodies through Immer, which hands you a special draft of the state. You "mutate" the draft freely, and Immer records those changes and produces a correct, brand-new immutable state behind the scenes.
⚠️ One rule to remember
Inside a slice reducer you may either mutate the draft or return a new value — never both in the same reducer. And this magic only applies inside RTK reducers; anywhere else (components, thunks) you still treat state as read-only.
Connecting to React
react-redux is the binding layer. You wrap your app in a <Provider>, read state with the useSelector hook, and send actions with the useDispatch hook.
Provide the store
// index.js (React 18+)
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import store from './app/store';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);
Read and dispatch in a component
// features/counter/Counter.jsx
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from './counterSlice';
export function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
const [amount, setAmount] = useState('2');
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<input value={amount} onChange={(e) => setAmount(e.target.value)} />
<button onClick={() => dispatch(incrementByAmount(Number(amount) || 0))}>
Add amount
</button>
</div>
);
}
✅ How re-rendering works
useSelector subscribes the component to the store and re-runs your selector after every dispatch. If the selected value changed (by reference/value), the component re-renders; if not, it's skipped. Select the smallest slice of state a component needs to avoid unnecessary renders.
Selectors & Entity Adapters
Memoized selectors with createSelector
RTK re-exports createSelector from Reselect. It builds memoized selectors that only recompute when their inputs change — ideal for derived data like filtered lists, so you don't recompute (and re-render) on unrelated updates.
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'COMPLETED': return todos.filter(t => t.completed);
case 'ACTIVE': return todos.filter(t => !t.completed);
default: return todos;
}
}
);
Normalized data with createEntityAdapter
For collections (users, posts, products) that you look up by id, createEntityAdapter stores data in a normalized { ids: [], entities: {} } shape and hands you ready-made CRUD reducers and selectors.
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
const usersAdapter = createEntityAdapter({
selectId: (user) => user.userId,
sortComparer: (a, b) => a.name.localeCompare(b.name)
});
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState({ status: 'idle', error: null }),
reducers: {
userAdded: usersAdapter.addOne,
usersReceived: usersAdapter.setAll,
userUpdated: usersAdapter.updateOne,
userRemoved: usersAdapter.removeOne
}
});
export const { userAdded, usersReceived, userUpdated, userRemoved } = usersSlice.actions;
export const {
selectAll: selectAllUsers,
selectById: selectUserById,
selectIds: selectUserIds
} = usersAdapter.getSelectors((state) => state.users);
export default usersSlice.reducer;
Best Practices
Organize by feature, not by type
Put each feature's slice and its components together, so everything about "todos" lives in one folder:
src/
app/
store.js
features/
counter/
counterSlice.js
Counter.jsx
todos/
todosSlice.js
TodoList.jsx
| ✅ Do | ❌ Don't |
|---|---|
Use configureStore and createSlice | Reach for deprecated createStore in new code |
Derive data with selectors / createSelector | Store computed values that can drift out of sync |
Put id/timestamp generation in a prepare callback | Call Date.now() or Math.random() in the reducer body |
| Consider RTK Query for server data | Hand-roll caching and loading flags for every endpoint |
| Add TypeScript for excellent inference | Ignore the free type-safety RTK provides |
💡 RTK Query in one sentence
For fetching, caching, and syncing server data, RTK ships RTK Query — you declare endpoints and it generates hooks like useGetUsersQuery() that handle loading, caching, and refetching for you. You'll meet the async foundations it's built on in the next lesson.
Hands-on Exercise
🏋️ Convert the Todo Store to a Slice
Objective: Replace the classic-Redux todo store (from the previous lesson) with a single RTK slice plus a filter slice and a memoized selector.
Instructions:
- Create
todosSlicewithaddTodo(use apreparecallback for the id/timestamp),toggleTodo, anddeleteTodo. - Create
filterSlicewith a singlesetFilterreducer. - Wire both into
configureStore. - Write a
selectFilteredTodosmemoized selector withcreateSelector. - Bonus: add a
clearCompletedreducer.
💡 Hint
Inside slice reducers you can mutate the draft directly: state.push(...), todo.completed = !todo.completed. For delete, reassign: state.items = state.items.filter(...). Keep the id/timestamp out of the reducer by generating them in prepare.
✅ Sample solution
// features/todos/todosSlice.js
import { createSlice, createSelector } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [] },
reducers: {
addTodo: {
reducer: (state, action) => { state.items.push(action.payload); },
prepare: (text) => ({
payload: { id: crypto.randomUUID(), text, completed: false }
})
},
toggleTodo: (state, action) => {
const t = state.items.find(t => t.id === action.payload);
if (t) t.completed = !t.completed;
},
deleteTodo: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
clearCompleted: (state) => {
state.items = state.items.filter(t => !t.completed);
}
}
});
export const { addTodo, toggleTodo, deleteTodo, clearCompleted } = todosSlice.actions;
export default todosSlice.reducer;
// features/filter/filterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const filterSlice = createSlice({
name: 'filter',
initialState: 'ALL',
reducers: { setFilter: (_state, action) => action.payload }
});
export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;
// selector
export const selectFilteredTodos = createSelector(
[(s) => s.todos.items, (s) => s.filter],
(items, filter) => {
if (filter === 'ACTIVE') return items.filter(t => !t.completed);
if (filter === 'COMPLETED') return items.filter(t => t.completed);
return items;
}
);
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
import filterReducer from '../features/filter/filterSlice';
export const store = configureStore({
reducer: { todos: todosReducer, filter: filterReducer }
});
🎯 Quick Quiz
Question 1: Which single RTK API generates a reducer, its action creators, and its action types together?
Question 2: Why is state.value += 1 safe inside an RTK slice reducer?
Question 3: Where should you generate a new todo's id and timestamp?
Summary & Quiz
🎉 Key Takeaways
- Redux Toolkit is the official, recommended way to write Redux — same principles, far less code.
configureStoregives you a store with DevTools, thunk, and dev checks by default.createSlicegenerates the reducer, action creators, and action types from one object.- Immer lets you write natural "mutating" logic that produces correct immutable state.
- Connect with
Provider+useSelector+useDispatch; derive data withcreateSelector; normalize collections withcreateEntityAdapter.
📚 Further Reading
- Redux Toolkit — Official Docs
- createSlice API Reference
- Redux Essentials — App Structure with RTK
- Immer documentation
🚀 What's Next?
Everything so far has been synchronous. Real apps need to fetch data. Next you'll handle asynchronous operations in Redux with thunks and createAsyncThunk, and see how RTK Query builds on those foundations.
🎉 Great progress!
You can now write real-world Redux quickly. Time to make it talk to a server.