React.js Fundamentals

Introduction to Redux Toolkit

20 min Lesson 22 of 40

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:

  1. Create cartSlice with items array, total, and discount
  2. Implement actions: addItem, removeItem, updateQuantity, applyDiscount, clearCart
  3. Create selectors: selectCartItems, selectCartTotal, selectItemCount
  4. Build Cart component displaying items with quantity controls
  5. Add CartSummary showing total and discount

Practice Exercise 2: User Authentication State

Build an auth slice:

  1. Create authSlice with user, token, isAuthenticated, isLoading
  2. Add actions: loginStart, loginSuccess, loginFailure, logout
  3. Create selectors: selectUser, selectIsAuthenticated, selectAuthError
  4. Build LoginForm component dispatching login actions
  5. Create ProtectedRoute component checking auth state

Practice Exercise 3: Notification System

Create a notification management slice:

  1. Build notificationSlice with array of notifications
  2. Implement addNotification (with auto-generated ID), removeNotification, clearAll
  3. Use prepare callback to add timestamp and type (success/error/warning/info)
  4. Create NotificationList component displaying all notifications
  5. 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.