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?
Real-world analogy
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
- Local state first — If one component or a close parent owns the data, use useState there. Reach for shared tools only when passing props gets painful.
- Context shares slow values — It suits values many components read and that rarely change, such as theme, language or the current user. A provider supplies the value to its whole subtree.
- Context re-renders consumers — When the provider value changes, every component that reads it re-renders. A new object literal as value counts as a change whenever the provider renders.
- Redux Toolkit for busy state — Use it for shared client state that changes often, such as a cart or a filter set. Many pages read it. It adds structure, tooling and selectors that limit re-renders.
- configureStore and createSlice — createSlice bundles a name, initial state and reducers, and it generates the actions. configureStore combines the slice reducers into one store.
- Immer allows safe mutation — Inside a slice reducer you can write state.push(item). Immer records the changes and produces a new immutable state.
- Typed hooks — Export RootState and AppDispatch from the store, then create typed versions of useSelector and useDispatch. Components then get autocomplete and type errors.
- Server data is different — Data that lives on the server needs caching, loading flags and refetching. A data layer such as RTK Query or TanStack Query handles that. Do not copy it by hand into a slice.
- Answering Context or Redux — Say it depends on how often the value changes and how many parts read it. Start local, use Context for slow shared values, and add Redux Toolkit when updates are frequent and shared.
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. This is one file. CartItem is the shape of one cart line, and the initial state is a typed empty array.
- 2. createSlice names the slice cart and defines two reducers. added mutates the draft state, which Immer turns into a safe update.
- 3. removed returns a new array instead, which is the other style a reducer can use.
- 4. configureStore combines the slice reducer under the key cart. The slice also generates the added and removed actions.
- 5. RootState and AppDispatch are read from the store, and withTypes creates hooks that know them.
- 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Passing Data Deeply with Context (React)
- Redux Toolkit Quick Start (Redux Toolkit)
- Free Full-Stack Playbook (React and TypeScript lessons) (Teamz Lab)
Lock it in