Global State with Redux Toolkit
Reviewed & published by Brayan K
When many unrelated parts of an app need the same data — the logged-in user, a shopping cart, a theme — passing props down through every layer gets painful. Redux puts that shared state in one predictable store. Redux Toolkit (RTK) is the official, boilerplate-free way to use it: createSlice generates your actions and lets you write update logic that looks mutable but stays immutable. This is the industrial-strength state pattern you'll see in large React codebases.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- The Redux model: store, action, reducer, dispatch
- Create a feature slice with createSlice
- Why RTK lets you write 'mutating' code safely (Immer)
- Configure the store with configureStore
- Read state with useSelector, update with useDispatch
- When Redux is worth it vs useState / Context
1️⃣ The Redux Model
Four ideas: the store holds state; an action describes "what happened"; a reducer computes the next state from the current state plus an action; dispatch sends an action through. The reducer is a pure (state, action) => newState function:
The demo models this engine — dispatch a few actions and watch the single state evolve:
// At its core, Redux is one function: (state, action) => newState.
// RTK generates this for you, but here's the engine, modeled in JS.
function counterReducer(state, action) {
switch (action.type) {
case "counter/increment": return { value: state.value + 1 };
case "counter/decrement": return { value: state.value - 1 };
case "counter/addBy": return { value: state.value + action.payload };
default: return state;
}
}
// dispatch = "run the reducer with this action, store the result"
let state = { value: 0 };
function dispatch(action) { state = counterReducer(state, action); }
dispatch({ type: "counter/increment" });
dispatch({ type: "counter/increment" });
dispatch({ type: "counter/addBy", payload: 10 });
console.log(state.value); // 12
// createSlice hides this switch and generates the action objects for you.2️⃣ createSlice — No Boilerplate
Classic Redux made you hand-write action type strings and immutable spreads. RTK's createSlice generates the action creators and, thanks to Immer, lets you write code that looks like mutation but produces a new immutable state:
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; }, // looks mutable…
decrement: (state) => { state.value -= 1; }, // …Immer makes it safe
addBy: (state, action) => { state.value += action.payload; },
},
});
// RTK auto-generates one action creator per reducer:
export const { increment, decrement, addBy } = counterSlice.actions;
export default counterSlice.reducer;Your turn — complete the reducer that appends an item to the cart array:
// 🎯 YOUR TURN — fill in the ___ then press "Try it Yourself".
// In a slice, each reducer returns the next state. Complete "addBy".
function reducer(state, action) {
switch (action.type) {
case "cart/addItem":
// 1) Add the item from action.payload onto the items array.
return { items: [...state.items, ___] }; // 👉 use action.payload
default:
return state;
}
}
let state = { items: [] };
state = reducer(state, { type: "cart/addItem", payload: "shoes" });
state = reducer(state, { type: "cart/addItem", payload: "hat" });
console.log(state.items);
// ✅ Expected output:
// [ 'shoes', 'hat' ]3️⃣ Store, Provider, and the Hooks
Combine your slices into one store, wrap the app in a Provider, then read and write from any component:
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
export const store = configureStore({
reducer: { counter: counterReducer }, // state.counter
});
// main.tsx — wrap once
import { Provider } from "react-redux";
<Provider store={store}><App /></Provider>
// Counter.tsx — read + write anywhere
import { useSelector, useDispatch } from "react-redux";
import { increment, addBy } from "./counterSlice";
function Counter() {
const count = useSelector((state) => state.counter.value); // subscribe
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(addBy(10))}>+10</button>
</div>
);
}A selector reads just the slice a component needs — so it only re-renders when that value changes. The demo models several selectors over one store:
// useSelector reads a SLICE of the whole store. We model the store as
// one object and selectors as functions that pick a piece out of it.
const store = {
counter: { value: 7 },
user: { name: "Ada", isAdmin: true },
cart: { items: ["book", "pen"] },
};
// Each selector grabs exactly what a component needs:
const selectCount = (s) => s.counter.value;
const selectUserName = (s) => s.user.name;
const selectCartCount = (s) => s.cart.items.length;
console.log("count:", selectCount(store)); // count: 7
console.log("user:", selectUserName(store)); // user: Ada
console.log("cart size:", selectCartCount(store)); // cart size: 2
// A component re-renders only when ITS selected value changes.Reorder Challenge
These lines of a createSlice call are scrambled. Put them in the correct order:
A. reducers: {
B. const slice = createSlice({
C. increment: (state) => { state.value += 1; },
D. initialState: { value: 0 },
E. },
F. name: "counter",
G. });Quick Recall
1) How does a component read a value from the Redux store?
useSelector((state) => state.slice.value) — it subscribes the component to just that selected piece.
2) Why is state.value += 1 allowed inside a slice reducer?
RTK wraps reducers with Immer, which turns your "mutations" into a correct immutable update behind the scenes.
3) You changed state.user.name in the store. Which components re-render?
Only those whose useSelector result actually changed — e.g. ones selecting the user name. Components selecting unrelated slices don't re-render.
Common Errors (and the fix)
- Mutating state outside RTK: Immer only works inside slice reducers. Anywhere else, return a new object/array.
- Forgetting the <Provider>: "could not find react-redux context" means the app isn't wrapped in <Provider store={store}>.
- Selecting too much: useSelector(s => s) re-renders on every change. Select the narrowest value you need.
- Returning a new object from a selector each call: s => ({...}) breaks equality checks. Memoize with createSelector or select primitives.
📋 Quick Reference
| Task | Syntax |
|---|---|
| Create a slice | createSlice({ name, initialState, reducers }) |
| Build the store | configureStore({ reducer: { counter } }) |
| Provide it | <Provider store={store}> |
| Read state | useSelector(s => s.counter.value) |
| Dispatch | dispatch(increment()) |
Mini-Challenge: Toggle Slice
Model a tiny theme slice reducer with a toggle action that flips between "light" and "dark".
// A theme slice reducer that toggles light <-> dark.
function reducer(state, action) {
if (action.type === "theme/toggle") {
// your code here: return { mode: state.mode === "light" ? "dark" : "light" }
}
return state;
}
let state = { mode: "light" };
state = reducer(state, { type: "theme/toggle" });
console.log(state.mode);
state = reducer(state, { type: "theme/toggle" });
console.log(state.mode);
// 🎯 Expected:
// dark
// light🎉 Lesson Complete
- ✅ Redux = store + action + reducer + dispatch
- ✅ createSlice generates actions and uses Immer for safe "mutations"
- ✅ configureStore + <Provider> wire it into the app
- ✅ Read with useSelector, update with useDispatch
- ✅ Reach for it when shared state is large or complex
Practice quiz
What is Redux Toolkit (RTK)?
- A CSS-in-JS library
- A replacement for React
- The official, recommended way to write Redux with less boilerplate
- A router
Answer: The official, recommended way to write Redux with less boilerplate. RTK is the official toolset; createSlice generates action types/creators and reduces boilerplate.
What is the shape of a Redux reducer?
- (state, action) => newState
- () => state
- (action) => state
- (state) => action
Answer: (state, action) => newState. A reducer is a pure function (state, action) => newState.
What does createSlice do?
- Creates a React component
- Connects to a database
- Configures routing
- Bundles a feature's initialState and reducers, auto-generating action creators
Answer: Bundles a feature's initialState and reducers, auto-generating action creators. createSlice({ name, initialState, reducers }) generates an action creator per reducer.
Why is state.value += 1 allowed inside a slice reducer?
- Redux always allows mutation
- RTK wraps reducers with Immer, which turns 'mutations' into a correct immutable update
- It's a syntax error that React fixes
- Because value is a primitive
Answer: RTK wraps reducers with Immer, which turns 'mutations' into a correct immutable update. Immer records the mutations and builds a new immutable state behind the scenes — only inside slice reducers.
How does a component read a value from the store?
- useSelector((state) => state.counter.value)
- useDispatch
- useStore directly
- useState
Answer: useSelector((state) => state.counter.value). useSelector subscribes the component to just the selected slice of state.
How does a component update the store?
- Mutate state directly
- Call the reducer directly
- const dispatch = useDispatch(); dispatch(increment())
- setState
Answer: const dispatch = useDispatch(); dispatch(increment()). Dispatch an action creator's result; the store runs the reducer and notifies subscribers.
What does configureStore do?
- Creates a slice
- Combines reducers into one store (with good defaults)
- Provides the store to React
- Reads state
Answer: Combines reducers into one store (with good defaults). configureStore({ reducer: { counter } }) builds the store from your slice reducers.
What wires the Redux store into the React app?
- <Router>
- <StoreContext>
- configureStore alone
- <Provider store={store}>
Answer: <Provider store={store}>. Wrapping the app in react-redux's <Provider store={store}> makes the store available to hooks.
After a store update, which components re-render?
- All of them
- Only those whose useSelector result actually changed
- None until refresh
- Only the dispatching one
Answer: Only those whose useSelector result actually changed. Components re-render only when their selected value changes; unrelated selectors don't.
Where is mutating state with Immer-style syntax valid?
- Anywhere in the app
- Only in selectors
- Only inside RTK slice reducers; elsewhere return a new object/array
- In useEffect
Answer: Only inside RTK slice reducers; elsewhere return a new object/array. Immer only applies inside slice reducers; outside, you must produce a new immutable value.
Continue this course
- Previous: Forms with React Hook Form + Zod
- Next: Lightweight State with Zustand — Simple, fast global state without the boilerplate
- Quick reference: React cheat sheet