Module 5

React State: Context API & Redux

How to share state across many components. First the built-in Context API, then Redux Toolkit for larger apps, including async data, RTK Query and setup in a Next.js App Router project.

Context API useReducer Redux Toolkit RTK Query Next.js setup
Level 1 — BasicsThe problem, Context API fundamentals and typed custom hooks

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.

Without Context — prop drilling
App (user) user Layout user Sidebar user Avatar ✓ doesn'tneed it!
With Context — direct access
UserProvider Layout Sidebar Avatar ✓ useContext

Which state tool should I use? Basic

Decision guide
Is the data from the server (API / DB)?Yes → Server Components, SWR / TanStack Query or RTK Query
No, it's UI / client state
Used by one component (or its child)?Yes → useState / useReducer
No, shared widely
Changes rarely? (theme, user, locale)Yes → Context API
No, complex / frequent updates
Big app, many features, devtools, middleware→ Redux Toolkit (or Zustand)

Part A — Context API

Built into React. No libraries needed.

Context in 3 steps Basic

Radio stationThe Provider is a radio tower broadcasting a value. Any component inside its range can tune in with useContext. Components outside the range only hear the default value.
Create → Provide → Consume
createContext()make the "channel"
wrap tree
<Ctx value={…}>broadcast a value
any child
useContext(Ctx)read the value
React 19 shortcutFrom React 19 you can render the context itself as the provider: <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.

Level 2 — IntermediateContext + useReducer, Next.js providers, and Redux Toolkit fundamentals

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.

reducer(state, action) → newState
dispatch({ type: "add", item })
action
cartReducerpure function
returns
new stateUI re-renders

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.

Where providers live
app/layout.tsx (Server)fetches the session, renders <Providers>
app/providers.tsx ("use client")ThemeProvider → AuthProvider → CartProvider
{children} pagesstill Server Components ✓

Part B — Redux Toolkit

A predictable, centralized store for large apps. Redux Toolkit (RTK) is the official, modern way to write Redux.

Redux core concepts Intermediate

Bank analogyThe store is the bank vault, the single source of truth. You can't walk in and change your balance. You fill in a deposit slip (an action) and hand it to the teller (dispatch). The bank's rules (the reducer) calculate the new balance. You check your balance through the app (a selector).
One-way data flow
🖥️ UI / Componentuser clicks "Add" 📨 Action{ type: "cart/add" } ⚙️ Reducer(state, action) → new 🏦 Store (state)single source of truth dispatch store calls new state useSelector
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.

state.counter.value
0
Action log
// click a button…

Redux Toolkit setup in Next.js Intermediate

Files we'll create
  • 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
Next.js rules for Redux
  • 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
VS
Redux Toolkit
  • createSlice does all of it
  • Immer for "mutable" updates
  • configureStore includes thunk and devtools
  • Much less code
Level 3 — AdvancedAsync thunks, RTK Query, memoized selectors, entity adapters, middleware and performance

Async logic with createAsyncThunk Advanced

Reducers must stay pure, so API calls go in thunks. createAsyncThunk dispatches pending, then fulfilled or rejected, automatically.

Thunk lifecycle
dispatch(fetchProducts())
immediately
…/pendingstatus = "loading"
API resolves
…/fulfilledsave data
…/rejectedsave error

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.

Tag-based cache invalidation
useGetProductsQuery()provides tag "Product"
cached
useAddProductMutation()invalidates "Product"
auto re-fetch
List updates by itself

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)

Array → normalized
[ {id:1,…}, {id:2,…} ]find() is O(n)
normalize
{ ids: [1,2], entities: {1:{…}, 2:{…}} }lookup is O(1)

Middleware, listeners & persistence Advanced

Middleware sits between dispatch and reducer
dispatch(action)
logger
thunk
listener
reducer
Redux DevToolsInstall the browser extension to see every action, inspect the state diff, and use "time travel" to step back through actions. 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.

Re-renders all consumers every time
Stable reference

Context vs Redux — side by side Advanced

Aspect Context API Redux Toolkit
InstallBuilt into React@reduxjs/toolkit react-redux
PurposeA way to pass values down the treeA full state management system
Re-rendersAll consumers when the value changesOnly components whose selected slice changed
BoilerplateMinimalSmall with RTK
DevTools / time travel✗✓
Middleware / side effectsManual (useEffect)Thunks, listeners, RTK Query
Best forTheme, auth user, locale, small/medium appsLarge apps, complex shared state, many developers
Other optionsZustand is tiny and hook-based, and Jotai builds state from small atoms. Both are popular lighter alternatives. In Next.js App Router apps, much of the "global" state moves to the server (Server Components + caching), so you often need less client state than you expect.
Practice & ReviewQuiz, interview questions and a cheat sheet

Interview questions

Where should an API call go in Redux Toolkit?

In the Next.js App Router, a Context Provider must be in…

Not exactly. Context is a transport mechanism: it passes a value down the tree. The state itself lives in useState or useReducer. Together they act as state management.

Pure reducers are predictable and testable, and they make time-travel debugging possible. The same state and action always produce the same result.

RTK uses Immer. You change a draft proxy, and Immer records the changes and produces a new immutable state object.

The Next.js server handles many users' requests in one process. A module-level store would leak one user's state to another. Creating the store per request (or per client tab, using useRef) prevents that.

Practice projects

  1. Theme + language switcherTwo separate contexts with typed custom hooks.
  2. Cart with Context + useReducerThen rebuild it with Redux Toolkit and compare.
  3. 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>provide
  • useContext(Ctx)consume
  • useReducer(fn, init)complex state
  • useMemo(() => value)stable value
Redux Toolkit
  • configureStore()store
  • createSlice()state + reducers
  • PayloadAction<T>typed action
  • useAppSelector / Dispatchhooks
  • <Provider store>connect
Async
  • createAsyncThunk()thunk
  • extraReducershandle it
  • rejectWithValuetyped error
  • .unwrap()await result
RTK Query
  • createApi()api slice
  • build.query / mutationendpoints
  • providesTagscache tag
  • invalidatesTagsrefetch
Context & Redux notes