Redux is a predictable state container — all application state lives in a single JavaScript object (the store), state can only be updated by dispatching actions (plain objects describing what happened), and a pure reducer function specifies how the state changes. This one-way data flow makes state changes traceable, reproducible, and debuggable with time-travel tooling. Redux Toolkit (RTK) is the official, opinionated way to write Redux — it eliminates the historical boilerplate of action type constants, action creators, and switch-statement reducers, replacing them with concise, modern APIs.
The Redux Data Flow
Redux unidirectional data flow:
Component dispatches action
↓
dispatch({ type: "posts/likePost", payload: { postId: 42 } })
↓
Reducer receives (currentState, action)
↓
Returns new state (immutably)
↓
Store updates
↓
All subscribed components re-render with new state
Key properties:
• Single source of truth: one store for all state
• State is read-only: only actions can change it
• Changes via pure functions: reducers are predictable and testable
• Devtools: every action is recorded — replay, time-travel, inspect
createSlice uses Immer under the hood, which lets you write “mutating” code (state.posts.push(newPost)) that is actually converted to immutable updates behind the scenes. This is a major ergonomic improvement over plain Redux where you had to manually spread every nested object. Despite the appearance of mutation, the actual state is never mutated — Immer intercepts the writes and produces a new state object.console.log — you can see exactly which action caused a state change and what the state looked like at any point in time. It is free, takes 30 seconds to install, and is one of Redux’s biggest advantages over alternatives.Installation
npm install @reduxjs/toolkit react-redux
Plain Redux vs Redux Toolkit
// ── Plain Redux (old way) ─────────────────────────────────────────────────────
// Three separate files required: action types, action creators, reducer
// actionTypes.js
const LIKE_POST = "posts/LIKE_POST";
const UNLIKE_POST = "posts/UNLIKE_POST";
// actions.js
const likePost = (postId) => ({ type: LIKE_POST, payload: postId });
const unlikePost = (postId) => ({ type: UNLIKE_POST, payload: postId });
// reducer.js
function postsReducer(state = [], action) {
switch (action.type) {
case LIKE_POST:
return state.map(p =>
p.id === action.payload
? { ...p, likeCount: p.likeCount + 1, liked: true }
: p
);
default: return state;
}
}
// ── Redux Toolkit (modern way) ────────────────────────────────────────────────
// Everything in one createSlice call
import { createSlice } from "@reduxjs/toolkit";
const postsSlice = createSlice({
name: "posts",
initialState: { items: [], isLoading: false, error: null },
reducers: {
likePost(state, action) {
const post = state.items.find(p => p.id === action.payload);
if (post) { post.likeCount += 1; post.liked = true; }
// Immer makes "mutation" safe — this actually produces a new state
},
unlikePost(state, action) {
const post = state.items.find(p => p.id === action.payload);
if (post) { post.likeCount = Math.max(0, post.likeCount - 1); post.liked = false; }
},
},
});
export const { likePost, unlikePost } = postsSlice.actions;
export default postsSlice.reducer;
Common Mistakes
Mistake 1 — Mutating state outside createSlice (without Immer protection)
❌ Wrong — direct mutation in a regular reducer breaks Redux:
function reducer(state = [], action) {
state.push(action.payload); // mutation! Redux won't detect the change
return state; // same reference — UI won't update
}
✅ Correct — return new state, or use createSlice (Immer handles it):
function reducer(state = [], action) {
return [...state, action.payload]; // ✓ new array
}
Mistake 2 — Storing everything in Redux
❌ Wrong — Redux for local UI state (toggle, hover, form values):
// isDropdownOpen belongs in local useState, not global Redux store
dispatch({ type: "ui/openDropdown" });
✅ Correct — Redux for shared global state; useState for local component state.
Quick Reference
| Concept | RTK API |
|---|---|
| Create slice (state + reducers) | createSlice({ name, initialState, reducers }) |
| Create store | configureStore({ reducer: { posts: postsSlice.reducer } }) |
| Read state in component | const posts = useSelector(s => s.posts.items) |
| Dispatch action | const dispatch = useDispatch(); dispatch(likePost(42)) |
| Action creator | Auto-generated: postsSlice.actions.likePost |
| Wrap app | <Provider store={store}><App /></Provider> |