Redux Toolkit
The store, actions, reducers, createSlice with Immer and Redux in Next.
Updated
What Redux is
Redux is a global state model with strict rules:
- All global state lives in a single store.
- State isn't changed directly — you send (dispatch) an action: an object that describes what happened (
{ type: 'cart/add', payload: item }). - A reducer — a pure function
(state, action) => newState— computes the new state.
UIdispatch(action)action→reducer(state, action) => newStatenew state→storeselector→UIre-rendersThe benefit: every change goes through the same place → predictable, testable, with DevTools that show you every action and can "rewind" the history.
Redux Toolkit (RTK) is the official and current way to write Redux. "Classic" Redux (with switch, type constants, connect) is considered outdated.
The pieces of Redux Toolkit
| Piece | What it does |
|---|---|
configureStore |
creates the store, with DevTools and middleware included |
createSlice |
defines a piece of state + its reducers; generates the actions automatically |
useSelector |
reads a piece of the store (the component re-renders only when that piece changes) |
useDispatch |
sends actions |
createAsyncThunk |
async logic (rare today — for data you use RTK Query) |
| RTK Query | fetching + caching for server state — the next lesson |
A slice
// entities/cart/model/cart-slice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
type CartItem = { id: number; qty: number }
const cartSlice = createSlice({
name: 'cart',
initialState: [] as CartItem[],
reducers: {
added(state, action: PayloadAction<number>) {
const item = state.find(i => i.id === action.payload)
if (item) item.qty++ // an allowed "mutation" — see Immer
else state.push({ id: action.payload, qty: 1 })
},
removed(state, action: PayloadAction<number>) {
return state.filter(i => i.id !== action.payload)
},
},
})
export const { added, removed } = cartSlice.actions
export const cartReducer = cartSlice.reducerImmer: in createSlice you can write code that looks like it changes the state (state.push, item.qty++). RTK uses the Immer library, which turns these "mutations" into a new, immutable object. The rules from References and immutability still apply — only the syntax is shorter.
The store and typed hooks
// shared/store/store.ts
export const makeStore = () => configureStore({ reducer: { cart: cartReducer } })
export type AppStore = ReturnType<typeof makeStore>
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()'use client'
const count = useAppSelector(state => state.cart.length)
const dispatch = useAppDispatch()
<button onClick={() => dispatch(added(product.id))}>Add</button>Redux with the Next.js App Router
- The store is created per request, not as a module-level global variable — otherwise, on the server, one user's data would reach another. That's why
makeStore()is a function. - You put it in a Client Component
StoreProvider('use client'), created once withuseRef/useState. - Server Components can't read the store — they get their data directly.
When to use Redux Toolkit
| Yes | No |
|---|---|
| a big app, many teams, strict rules are useful | a small / medium app |
| complex logic with many transitions, a need for DevTools / time-travel | just a bit of global UI state |
| an existing Redux project | state that's actually server state |
Summary
- Redux = one store, actions, pure reducers; RTK is the current way to write it.
createSlice+ Immer = short code, an immutable result.- In Next: a store per request, inside a Client Component; for server data → RTK Query.