Welcome to the big leagues. If you’ve been following along, we’ve solved the prop-drilling problem using the Context API, and we even looked at Recoil to prevent unnecessary re-renders.
So right now, a very valid question is probably popping up in your head: “If Recoil and Context API solve my problems, why on earth am I learning Redux?”
Let’s clear that up before we write a single line of code.
❓ Why Redux instead of Context or Recoil?
- Context API is great for passing data deeply, but it’s not a state management tool. Whenever a Context value changes, every component consuming that Context re-renders. That’s a performance nightmare for large apps.
- Recoil solves the re-render issue beautifully with “Atoms.” It’s lightweight and React-native. But here is the brutal truth: Redux is the undisputed industry standard.
- If you get a job as a React developer tomorrow, there is a 90% chance the codebase uses Redux.
- Redux enforces a strict, predictable Unidirectional Data Flow. In enterprise apps with hundreds of components, this strictness prevents chaotic bugs.
- Redux DevTools: The debugging experience in Redux is practically magic. You can literally “time-travel” through your app’s state changes. Recoil cannot do this at the same level.
1. The Story of Redux (And Why We Use “Toolkit”)
To understand Redux, you need to know a tiny bit of history.
The Dark Ages: Flux
Years ago, Facebook created an architecture called Flux to handle state. It introduced the idea of a one-way data flow, but it allowed multiple stores. It was messy and hard to maintain.
The Renaissance: Vanilla Redux (2015)
Dan Abramov and Andrew Clark took the ideas of Flux and perfected them into Redux. They introduced the golden rule: The Single Source of Truth. Your entire application’s state lives in ONE massive JavaScript object.
- The Problem: Vanilla Redux was notoriously difficult to set up. It required massive amounts of “boilerplate” code. You had to create action types, action creators, reducers, and manually install middleware like
redux-thunkjust to make an API call. If you forgot to copy an old state array before updating it, your app broke.
The Modern Era: Redux Toolkit (RTK)
To stop developers from pulling their hair out, the Redux team created Redux Toolkit (RTK). RTK is the official, opinionated, “batteries-included” way to write Redux.
- It writes the boilerplate for you.
- It sets up the Redux DevTools automatically.
- The biggest superpower: It includes a library called Immer.js under the hood. In vanilla Redux, directly mutating state (
state.push(newItem)) was a cardinal sin. In RTK, you can write mutating code, and Immer safely translates it into immutable updates behind the scenes!
2. The Core Mental Model
Before we code, burn these three concepts into your brain:
- The Store: The global database for your frontend. It holds everything.
- Reducers (Slices): The only functions allowed to change the Store. You don’t update the Store directly; you ask a Reducer to do it for you.
- Dispatch & Selectors:
- Dispatch (
useDispatch): The delivery boy. When a user clicks “Add Todo”, youdispatchan action to the Reducer. - Selector (
useSelector): The spyglass. How a component “selects” or reads specific data from the Store.
- Dispatch (
Wait, is Redux a React thing?
No. Redux is an independent JavaScript library. You can use it with Vue, Angular, or vanilla JS. To make it work with React, we need a bridge library called react-redux.
3. Let’s Build: A Redux Toolkit Todo App
We are going to build a Todo app. Let’s install the two packages we need:
npm install @reduxjs/toolkit react-redux
Step 1: Create the Store (src/app/store.js)
Every Redux app starts with a store. This is the easiest part.
import { configureStore } from '@reduxjs/toolkit';
// We will import our reducers here later
export const store = configureStore({
reducer: {} // The store needs to know about all the reducers we create
});
configureStore: This RTK method does the heavy lifting. It creates the store and automatically wires up the Redux DevTools extension for you.
Step 2: Create a “Slice” (src/features/todo/todoSlice.js)
In RTK, we organize our state into “Slices” (e.g., Auth Slice, Product Slice, Todo Slice). A slice contains the initial state and the reducers for that specific feature.
import { createSlice, nanoid } from '@reduxjs/toolkit';
// 1. How does the state look when the app first loads?
const initialState = {
todos: [{ id: 1, text: "Learn Redux Toolkit" }]
};
// 2. Create the slice
export const todoSlice = createSlice({
name: 'todo', // This name shows up in the Redux DevTools
initialState, // Attach the initial state
reducers: {
// Reducers take TWO arguments: (state, action)
addTodo: (state, action) => {
const newTodo = {
id: nanoid(), // RTK gives us nanoid to generate unique IDs instantly!
text: action.payload
};
// Wait, we are mutating state directly?!
// YES! RTK uses Immer.js behind the scenes. This is perfectly safe here.
state.todos.push(newTodo);
},
removeTodo: (state, action) => {
// action.payload will contain the ID of the todo we want to remove
state.todos = state.todos.filter((todo) => todo.id !== action.payload);
}
}
});
// 3. EXPORTING (Pay close attention, this is where beginners get stuck)
// Export the individual functions so our components can use them
export const { addTodo, removeTodo } = todoSlice.actions;
// Export the MAIN reducer so the Store can register it
export default todoSlice.reducer;
🧠 Developer Insight: state vs action.payload
state: Gives you access to the current values in this slice. Want to know what todos currently exist? Look instate.todos.action: When a component callsaddTodo("Buy Milk"), that string “Buy Milk” gets attached toaction.payload. Thepayloadis the data you pass in.
Step 3: Register the Slice in the Store (src/app/store.js)
Let’s go back to our store and tell it about our new slice.
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from '../features/todo/todoSlice'; // Import the default export
export const store = configureStore({
reducer: {
todos: todoReducer // Now the store is aware of our todo feature!
}
});
Step 4: Wrap the App (src/main.jsx)
React doesn’t know about Redux yet. We have to wrap our app in a <Provider> from react-redux.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { Provider } from 'react-redux'
import { store } from './app/store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>,
)
4. Connecting Components to Redux
The backend of our frontend is done. Now, how do our React components actually talk to this store?
Writing Data: useDispatch (AddTodo.jsx)
To send data to the store, we need the useDispatch hook. You can’t just call addTodo() normally; you have to dispatch it.
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { addTodo } from '../features/todo/todoSlice'
function AddTodo() {
const [input, setInput] = useState('')
const dispatch = useDispatch()
const addTodoHandler = (e) => {
e.preventDefault()
// We DISPATCH the action, and pass our input as the payload
dispatch(addTodo(input))
setInput('') // Clean up the form
}
return (
<form onSubmit={addTodoHandler}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter a Todo..."
/>
<button type="submit">Add Todo</button>
</form>
)
}
export default AddTodo
Reading Data: useSelector (Todos.jsx)
To read data from the store, we use useSelector. It gives us access to the entire global state object.
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { removeTodo } from '../features/todo/todoSlice'
function Todos() {
// Select the 'todos' array from the store
// (Remember we named it 'todos' inside configureStore's reducer object)
const todos = useSelector((state) => state.todos.todos)
const dispatch = useDispatch()
return (
<>
<h2>My Todos</h2>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
<button
// Dispatch removeTodo and pass the ID as the payload
onClick={() => dispatch(removeTodo(todo.id))}
>
Delete
</button>
</li>
))}
</ul>
</>
)
}
export default Todos
5. The Magic of Redux DevTools
If you followed along, your app is working. But you must install the Redux DevTools Extension in Chrome/Edge.
- Right-click your app, click Inspect, and go to the Redux tab.
- Add a Todo in your app.
- Look at the DevTools: You will see an action called
todo/addTodologged in the timeline. - Click the Diff tab. It will highlight exactly what changed in your state (e.g., green text showing a new object was added to the array).
- Look at the Slider at the bottom. You can grab it and slide backward in time to watch your Todos disappear and reappear on the UI exactly as you added them.
This traceability is exactly why enterprise companies love Redux. If a bug happens in a massive app, you can look at the Action Log and see exactly which payload corrupted the state.
🏋️♂️ Exercises to Build Muscle Memory
Quick Exercise (15 mins): Add a “Clear All” button.
- Go to
todoSlice.jsand add aclearTodos: (state) => {}reducer. What should the logic be to empty the array? - Export it from
.actions. - Create a button in the UI that dispatches it.
Intermediate Exercise (45 mins): Update a Todo
- Add an
updateTodoreducer. - Hint: The
action.payloadwill need to be an object:{ id: 1, newText: "Updated string" }. - Inside the reducer, use
state.todos.map()orstate.todos.find()to locate the todo by ID and change its.textproperty.
Challenge Exercise (2 Hours): Multi-Slice Architecture Create a brand new Vite project for a “Shopping Cart”.
- Create two slices:
productSlice(holds a hardcoded array of products) andcartSlice(holds items added to cart). - Register BOTH in
configureStore. - Create a component that lists products, and a button that
dispatchesthe product into thecartSlice. - Use
useSelectorto display a cart counter in a Navbar component.
🚨 Common Confusions & Pitfalls
| What you might think | The Reality |
|---|---|
“I need to use spread operators ...state to update arrays.” |
In Vanilla Redux, yes. In RTK, NO. Immer.js allows you to do state.todos.push(). It feels illegal, but it’s the intended RTK way. |
| “I forgot to export the reducer.” | This is the #1 beginner error. You must export the actions (destructured) AND export default mySlice.reducer. If you get a blank state error, check this first. |
| “Can I put API calls inside a Reducer?” | NO. Reducers must be “Pure Functions” (synchronous, no side effects). For async code, RTK uses something called createAsyncThunk (which we will cover in advanced Redux). |
"action.payload is undefined!" |
If you dispatch an action without passing an argument: dispatch(addTodo()), the payload is undefined. You must pass data: dispatch(addTodo("Hello")). |
📝 The Redux Toolkit Cheat Sheet
Keep this handy when you forget the flow:
- Setup:
npm i @reduxjs/toolkit react-redux - The Store:
configureStore({ reducer: { myFeature: mySliceReducer } }) - The Provider:
<Provider store={store}><App /></Provider> - The Slice:
createSlice({ name, initialState, reducers: { myFunc: (state, action) => {} } }) - Read State:
const data = useSelector(state => state.myFeature.data) - Write State:
const dispatch = useDispatch(); dispatch(myFunc(payload))
Once you wrap your head around the setup, Redux becomes a highly predictable, satisfying way to build massive applications. You provide the data, you dispatch the action, and Redux handles the rest perfectly every time.