Introduction to Redux Toolkit
Redux Toolkit (RTK) is the official, opinionated, batteries-included toolset for efficient Redux development. It simplifies Redux setup, reduces boilerplate, and includes best practices by default. RTK is now the recommended way to write Redux logic.
Why Redux Toolkit?
Traditional Redux required significant boilerplate code. Redux Toolkit solves common pain points:
Redux Toolkit Benefits:
- Simplified store setup with configureStore()
- Automatic Redux DevTools integration
- Built-in Immer for immutable updates
- createSlice() reduces boilerplate by 70%
- Includes createAsyncThunk for async logic
- TypeScript support out of the box
Installation and Setup
Install Redux Toolkit and React-Redux:
# Using npm
npm install @reduxjs/toolkit react-redux
# Using yarn
yarn add @reduxjs/toolkit react-redux
# Project structure
src/
├── store/
│ ├── index.js # Store configuration
│ ├── slices/
│ │ ├── counterSlice.js # Counter feature
│ │ ├── todoSlice.js # Todos feature
│ │ └── userSlice.js # User feature
│ └── hooks.js # Typed hooks
├── App.jsx
└── main.jsx
Creating Your First Store
Set up the Redux store with configureStore:
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slices/counterSlice';
import todoReducer from './slices/todoSlice';
import userReducer from './slices/userSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todoReducer,
user: userReducer
},
// DevTools enabled automatically in development
// Middleware includes thunk by default
});
// Infer types from store
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Creating Slices with createSlice
A slice represents a feature's Redux logic. createSlice generates actions and reducers automatically:
// store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
history: [],
step: 1
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// Redux Toolkit uses Immer, so you can "mutate" state directly
increment: (state) => {
state.value += state.step;
state.history.push({ action: 'increment', value: state.value });
},
decrement: (state) => {
state.value -= state.step;
state.history.push({ action: 'decrement', value: state.value });
},
// Actions can accept payloads
incrementByAmount: (state, action) => {
state.value += action.payload;
state.history.push({
action: 'incrementByAmount',
value: state.value,
amount: action.payload
});
},
setStep: (state, action) => {
state.step = action.payload;
},
reset: (state) => {
// Can return new state or mutate
return initialState;
},
clearHistory: (state) => {
state.history = [];
}
}
});
// Action creators are generated automatically
export const {
increment,
decrement,
incrementByAmount,
setStep,
reset,
clearHistory
} = counterSlice.actions;
// Export reducer
export default counterSlice.reducer;
// Selectors
export const selectCount = (state) => state.counter.value;
export const selectStep = (state) => state.counter.step;
export const selectHistory = (state) => state.counter.history;
Immer Magic: Redux Toolkit uses Immer library internally, which allows you to write "mutating" code that actually produces immutable updates. You can either mutate the draft state OR return a new state, but not both in the same reducer.
Using Redux in Components
Access state with useSelector and dispatch actions with useDispatch:
// components/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import {
increment,
decrement,
incrementByAmount,
setStep,
reset,
selectCount,
selectStep,
selectHistory
} from '../store/slices/counterSlice';
function Counter() {
const count = useSelector(selectCount);
const step = useSelector(selectStep);
const history = useSelector(selectHistory);
const dispatch = useDispatch();
const handleIncrement = () => {
dispatch(increment());
};
const handleDecrement = () => {
dispatch(decrement());
};
const handleIncrementByAmount = () => {
const amount = parseInt(prompt('Enter amount:'), 10);
if (!isNaN(amount)) {
dispatch(incrementByAmount(amount));
}
};
const handleStepChange = (e) => {
dispatch(setStep(parseInt(e.target.value, 10)));
};
const handleReset = () => {
dispatch(reset());
};
return (
<div className="counter">
<h2>Counter: {count}</h2>
<div className="controls">
<button onClick={handleDecrement}>-{step}</button>
<button onClick={handleIncrement}>+{step}</button>
<button onClick={handleIncrementByAmount}>
Add Custom Amount
</button>
<button onClick={handleReset}>Reset</button>
</div>
<div className="step-control">
<label>
Step Size:
<input
type="number"
value={step}
onChange={handleStepChange}
min="1"
/>
</label>
</div>
{history.length > 0 && (
<div className="history">
<h3>History</h3>
<ul>
{history.map((entry, index) => (
<li key={index}>
{entry.action}: {entry.value}
{entry.amount && ` (${entry.amount})`}
</li>
))}
</ul>
</div>
)}
</div>
);
}
Todo List with Redux Toolkit
A more complex example with filtering and multiple actions:
// store/slices/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
filter: 'all', // all, active, completed
nextId: 1
};
export const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.items.push({
id: state.nextId++,
text: action.payload,
completed: false,
createdAt: Date.now()
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
editTodo: (state, action) => {
const { id, text } = action.payload;
const todo = state.items.find(item => item.id === id);
if (todo) {
todo.text = text;
}
},
setFilter: (state, action) => {
state.filter = action.payload;
},
clearCompleted: (state) => {
state.items = state.items.filter(item => !item.completed);
},
toggleAll: (state) => {
const allCompleted = state.items.every(item => item.completed);
state.items.forEach(item => {
item.completed = !allCompleted;
});
}
}
});
export const {
addTodo,
toggleTodo,
deleteTodo,
editTodo,
setFilter,
clearCompleted,
toggleAll
} = todoSlice.actions;
export default todoSlice.reducer;
// Selectors
export const selectAllTodos = (state) => state.todos.items;
export const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = (state) => {
const { items, filter } = state.todos;
switch (filter) {
case 'active':
return items.filter(item => !item.completed);
case 'completed':
return items.filter(item => item.completed);
default:
return items;
}
};
export const selectTodoStats = (state) => {
const items = state.todos.items;
return {
total: items.length,
active: items.filter(item => !item.completed).length,
completed: items.filter(item => item.completed).length
};
};
Selector Performance: Complex selectors like selectFilteredTodos run on every render. For expensive computations, use the reselect library (included with RTK) to create memoized selectors that only recompute when their inputs change.
Creating Typed Hooks
Create typed versions of useDispatch and useSelector for better TypeScript support:
// store/hooks.js
import { useDispatch, useSelector } from 'react-redux';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;
// Usage in components
import { useAppDispatch, useAppSelector } from '../store/hooks';
function MyComponent() {
const dispatch = useAppDispatch();
const count = useAppSelector((state) => state.counter.value);
// ...
}
Prepare Callback for Complex Actions
Use the prepare callback when you need to customize action payloads:
// store/slices/postSlice.js
import { createSlice, nanoid } from '@reduxjs/toolkit';
export const postSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
addPost: {
reducer: (state, action) => {
state.push(action.payload);
},
// Prepare callback runs before reducer
prepare: (title, content, userId) => {
return {
payload: {
id: nanoid(), // Generate unique ID
title,
content,
userId,
createdAt: new Date().toISOString(),
reactions: { thumbsUp: 0, heart: 0 }
}
};
}
},
addReaction: (state, action) => {
const { postId, reaction } = action.payload;
const post = state.find(post => post.id === postId);
if (post) {
post.reactions[reaction]++;
}
}
}
});
// Usage
dispatch(addPost('My Title', 'Post content', userId));
// Automatically gets id, createdAt, reactions
Practice Exercise 1: Shopping Cart with Redux Toolkit
Create a shopping cart store:
- Create cartSlice with items array, total, and discount
- Implement actions: addItem, removeItem, updateQuantity, applyDiscount, clearCart
- Create selectors: selectCartItems, selectCartTotal, selectItemCount
- Build Cart component displaying items with quantity controls
- Add CartSummary showing total and discount
Practice Exercise 2: User Authentication State
Build an auth slice:
- Create authSlice with user, token, isAuthenticated, isLoading
- Add actions: loginStart, loginSuccess, loginFailure, logout
- Create selectors: selectUser, selectIsAuthenticated, selectAuthError
- Build LoginForm component dispatching login actions
- Create ProtectedRoute component checking auth state
Practice Exercise 3: Notification System
Create a notification management slice:
- Build notificationSlice with array of notifications
- Implement addNotification (with auto-generated ID), removeNotification, clearAll
- Use prepare callback to add timestamp and type (success/error/warning/info)
- Create NotificationList component displaying all notifications
- Add auto-dismiss after 5 seconds using setTimeout in component
Summary
Redux Toolkit drastically simplifies Redux development. configureStore sets up your store with good defaults, createSlice eliminates boilerplate, and Immer makes immutable updates easy. Use typed hooks for better TypeScript support, and leverage selectors for efficient state access. RTK is the modern, recommended way to use Redux.