The problem: prop drilling Basic
In React, data flows down through props. If a deeply nested component needs the logged-in user, every component in between has to pass it along, even components that never use it. This is called prop drilling.
Which state tool should I use? Basic
Context in 3 steps Basic
useContext. Components outside the range only hear the default value.<ThemeContext value={…}> instead of <ThemeContext.Provider value={…}>. React 19 also adds use(ThemeContext), which works like useContext but can be called inside if statements.Typed context + safe custom hook Basic
A best-practice pattern: default the context to null, and have the custom hook throw a clear error if someone forgets the Provider.
Context + useReducer: a shopping cart Intermediate
useReducer moves all the update logic into one pure function. Combined with Context, it gives you a "mini Redux" with no extra library.
Using Context in Next.js (App Router) Intermediate
Context only works in Client Components. Put all your providers in one "use client" file and wrap {children} with it in the root layout. Server Components passed as children stay Server Components.
Redux core concepts Intermediate
Store
One object that holds the whole app state.
Action
A plain object describing what happened: { type, payload }.
Reducer
A pure function that computes the next state. No side effects.
Dispatch
The only way to send an action to the store.
Selector
A function that reads a piece of state: s => s.cart.items.
Slice
The state, reducers and actions for one feature, created together with RTK.
Try it: a live mini Redux store
This demo runs a tiny Redux-style store in plain JavaScript on this page. Click the buttons and watch each action move through the reducer to produce new state.
Redux Toolkit setup in Next.js Intermediate
- lib/
- store.ts ← makeStore() + types
- hooks.ts ← typed useAppDispatch / useAppSelector
- features/
- counter/counterSlice.ts
- cart/cartSlice.ts
- app/
- StoreProvider.tsx ← "use client" wrapper
- layout.tsx
- No global store variable. On the server, a global store would be shared between different users' requests. Always use
makeStore(). - Server Components can't use Redux hooks. Only Client Components can read the store.
- Keep server data in Server Components or RTK Query. Use Redux for client state such as the cart, UI and filters.
createSlice & using it in components Intermediate
createSlice generates action creators and action types from your reducers. It uses Immer, so you can write code that looks like mutation (state.value += 1) and it still produces safe immutable updates.
Old Redux
- Action type constants
- Action creator functions
- switch-case reducers with spread
- Manual store + middleware setup
Redux Toolkit
createSlicedoes all of it- Immer for "mutable" updates
configureStoreincludes thunk and devtools- Much less code
Async logic with createAsyncThunk Advanced
Reducers must stay pure, so API calls go in thunks. createAsyncThunk dispatches pending, then fulfilled or rejected, automatically.
RTK Query: data fetching & caching Advanced
Writing thunks for every endpoint gets repetitive. RTK Query generates hooks that handle fetching, caching, loading states, deduplication and re-fetching for you.
Memoized selectors & normalized state Advanced
createSelector
If a selector returns a new array or object on every call (from filter or map), the component re-renders on every store change. createSelector memoizes the result, so it is only recalculated when its inputs change.
createEntityAdapter (normalized data)
Middleware, listeners & persistence Advanced
configureStore turns it on automatically in development.Performance & common pitfalls Advanced
Context: every consumer re-renders
When the Provider's value changes, all useContext consumers re-render. Split contexts (state vs dispatch) and wrap the value in useMemo.
Inline object as value
value={{ user }} creates a new object on every render, so every consumer re-renders. Memoize it.
Redux: select small pieces
useAppSelector(s => s.cart.items.length) is better than selecting the whole s.cart.
Keep state serializable
No class instances, Dates, Maps or functions in Redux. Store ISO strings and plain objects.
Don't put everything in Redux
Form inputs and a modal's open state usually belong in local useState.
Don't duplicate server data
Use RTK Query or TanStack Query instead of copying API data into slices by hand.
Context vs Redux — side by side Advanced
| Aspect | Context API | Redux Toolkit |
|---|---|---|
| Install | Built into React | @reduxjs/toolkit react-redux |
| Purpose | A way to pass values down the tree | A full state management system |
| Re-renders | All consumers when the value changes | Only components whose selected slice changed |
| Boilerplate | Minimal | Small with RTK |
| DevTools / time travel | ✗ | ✓ |
| Middleware / side effects | Manual (useEffect) | Thunks, listeners, RTK Query |
| Best for | Theme, auth user, locale, small/medium apps | Large apps, complex shared state, many developers |
Interview questions
Where should an API call go in Redux Toolkit?
In the Next.js App Router, a Context Provider must be in…
useState or useReducer. Together they act as state management.useRef) prevents that.Practice projects
- Theme + language switcherTwo separate contexts with typed custom hooks.
- Cart with Context + useReducerThen rebuild it with Redux Toolkit and compare.
- Product admin with RTK QueryList, add and delete products against your Next.js API (see the Backend page), with tag invalidation.
Cheat sheet
Context
createContext<T|null>(null)create<Ctx.Provider value>provideuseContext(Ctx)consumeuseReducer(fn, init)complex stateuseMemo(() => value)stable value
Redux Toolkit
configureStore()storecreateSlice()state + reducersPayloadAction<T>typed actionuseAppSelector / Dispatchhooks<Provider store>connect
Async
createAsyncThunk()thunkextraReducershandle itrejectWithValuetyped error.unwrap()await result
RTK Query
createApi()api slicebuild.query / mutationendpointsprovidesTagscache taginvalidatesTagsrefetch