Lesson 33 of 45 intermediate

Context API and Redux Toolkit

Keep state local first, then pick the lightest tool that shares it

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

A Context provider passes value={{ user, logout }} and its parent re-renders often. What happens?

Open interactive version (quiz + challenge)

Real-world analogy

A school has three ways to share news. A teacher tells the class directly, which is local state. A notice on the hall wall suits slow-changing facts like term dates. Everyone who looks at it must re-read it when it changes. Fast-changing shared facts go to a front office. It has one ledger, a clear way to write in it, and a list of who gets told. The wall notice is Context, and the front office is a Redux store.

What is it?

Context hands a value to a whole subtree of components without passing props at each level. Redux Toolkit keeps shared client state in one store. It changes state through named actions, and components subscribe to just the slice they need. Context solves passing data down. Redux Toolkit solves organising and updating data that many parts of the app share.

Real-world relevance

A shop app uses Context for the theme and the signed-in user, because those change rarely. The cart changes on each click and shows in the header, the product page and checkout. So it lives in a Redux Toolkit slice. The product list comes from the API and is cached by RTK Query instead of being stored in a slice. When someone asks why, the answer is about change frequency and who reads the data.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

Why can a Redux Toolkit reducer write state.push(item) safely?

Which data fits Context best?

Code example

import { configureStore, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { useDispatch, useSelector } from 'react-redux';

type CartItem = { id: number; name: string; qty: number };

const cartSlice = createSlice({
  name: 'cart',
  initialState: [] as CartItem[],
  reducers: {
    added(state, action: PayloadAction<CartItem>) {
      const existing = state.find(i => i.id === action.payload.id);
      if (existing) existing.qty += 1;
      else state.push(action.payload);
    },
    removed(state, action: PayloadAction<number>) {
      return state.filter(i => i.id !== action.payload);
    },
  },
});

export const { added, removed } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

export function CartBadge() {
  const count = useAppSelector(s => s.cart.reduce((n, i) => n + i.qty, 0));
  const dispatch = useAppDispatch();
  const addTea = () => dispatch(added({ id: 1, name: 'Tea', qty: 1 }));
  return <button type="button" onClick={addTea}>Cart ({count})</button>;
}

Line-by-line walkthrough

  1. 1. This is one file. CartItem is the shape of one cart line, and the initial state is a typed empty array.
  2. 2. createSlice names the slice cart and defines two reducers. added mutates the draft state, which Immer turns into a safe update.
  3. 3. removed returns a new array instead, which is the other style a reducer can use.
  4. 4. configureStore combines the slice reducer under the key cart. The slice also generates the added and removed actions.
  5. 5. RootState and AppDispatch are read from the store, and withTypes creates hooks that know them.
  6. 6. CartBadge selects only the count it needs and dispatches an action on click. The app root must wrap it in a Provider with the store, not shown here.

Spot the bug

// assume: createContext, useState and ReactNode are imported from 'react'
const ThemeContext = createContext<{ dark: boolean; toggle: () => void } | null>(null);

function ThemeProvider({ children }: { children: ReactNode }) {
  const [dark, setDark] = useState(false);
  return (
    <ThemeContext.Provider value={{ dark, toggle: () => setDark(d => !d) }}>
      {children}
    </ThemeContext.Provider>
  );
}
Need a hint?
Is the object in value the same object on each render of ThemeProvider?
Show answer
The value is a new object on every render of ThemeProvider, so whenever its parent re-renders, every consumer re-renders, even ones wrapped in memo, though dark did not change. Fix: wrap the value in useMemo with [dark] as the dependency, or pass a stable value.

Explain like I'm 5

Sometimes a whole class needs to know the same thing. You can pin a note on the wall, which is fine if it rarely changes. If the news changes all day, you need an office with one big book. It also needs a clear way to write in it.

Fun fact

Redux Toolkit exists because writing Redux by hand needed too much boilerplate. The Redux team now recommends it as the standard way to use Redux.

Hands-on challenge

Say it out loud in 30 seconds: "When would you pick Context, and when Redux Toolkit?" Then check: did you mention local state first, how often the value changes, consumers re-rendering, and server data in a separate layer?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook