Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Context API
Context provides a way to share data across the component tree without manually passing props at every level. It’s React’s built-in solution for “global” state. Real-world analogy: Imagine a building where the Wi-Fi password is posted in the lobby (the Provider). Any room (component) in the building can check the lobby sign directly — nobody needs to relay the password door-to-door from room to room. That relay process is prop drilling, and Context is the lobby sign that makes it unnecessary.The Problem: Prop Drilling
Without Context, passing data to deeply nested components requires threading props through every intermediate component — even the ones that do not use the data themselves:Creating and Using Context
Step 1: Create the Context
Step 2: Provide the Context
Step 3: Consume the Context
Complete Example: Theme Context
Here’s a full implementation with toggling:Real-World Pattern: Auth Context
Using Auth Context
Multiple Contexts
You can compose multiple contexts:Alternative: Compose Providers
Performance Optimization
The Re-render Problem
This is the biggest gotcha with Context and the reason senior developers sometimes avoid it for frequently-changing state. When context value changes, ALL consuming components re-render, even if they only use part of the context. There is no built-in way to subscribe to just a slice of the context value.Solution 1: Split Contexts
Solution 2: Memoize Context Value
Solution 3: State and Dispatch Separation
Context with useReducer
For complex state, combine Context with useReducer:Context Pitfalls
When to Use Context vs. Other Solutions
| Scenario | Recommendation |
|---|---|
| Theme, locale, user data | ✅ Context |
| Data needed by 2-3 components | Props (avoid over-engineering) |
| Complex state with many actions | Context + useReducer or Redux |
| Server state (API data) | React Query / SWR |
| Frequent updates (animations) | Zustand, Jotai, or refs |
| Very large apps | Redux Toolkit, Zustand |
🎯 Practice Exercises
Exercise 1: Notification Context
Exercise 1: Notification Context
Exercise 2: Language/i18n Context
Exercise 2: Language/i18n Context
Summary
| Concept | Description |
|---|---|
| Context | Share data without passing props manually |
| createContext | Creates a context object |
| Provider | Component that provides the value |
| useContext | Hook to consume context |
| Custom Hook | Best practice for consuming context |
| Prop Drilling | Problem Context solves |
| Performance | Split contexts to prevent unnecessary re-renders |
| useReducer + Context | Pattern for complex state management |
Next Steps
In the next chapter, you’ll learn about React Router — building multi-page applications with client-side routing!
Interview Deep-Dive
When a Context provider's value changes, every consumer re-renders. How do you prevent unnecessary re-renders in components that only use part of the context?
When a Context provider's value changes, every consumer re-renders. How do you prevent unnecessary re-renders in components that only use part of the context?
Strong Answer:
This is the fundamental performance limitation of Context, and there is no built-in way to subscribe to a “slice” of context. When the Provider’s value changes (by reference), every component calling
useContext for that context re-renders — even if it only reads theme and cart was the thing that changed.Three solutions, each with different tradeoffs:First, split contexts. Instead of one AppContext with theme and cart, create ThemeContext and CartContext. Components consuming only theme are unaffected by cart changes. This is the simplest and most recommended approach. The cost is more Provider components wrapping your app, but that has negligible runtime impact.Second, memoize the context value. Wrap the value object in useMemo: const value = useMemo(() => ({ theme, toggleTheme }), [theme]). This prevents re-renders when the provider component re-renders for unrelated reasons (like a parent state change), because the value reference stays stable. But it does NOT prevent re-renders when the actual context data changes — that is by design.Third, separate state and dispatch contexts. Inspired by Redux, you create two contexts: one for the state (which changes), one for the dispatch function (which is stable). Components that only dispatch actions (like an “Add to Cart” button) consume the dispatch context and never re-render when state changes. Components that read state consume the state context. This dramatically reduces re-renders in action-heavy UIs.What most people miss: you can also use React.memo on child components of the consumer to prevent their subtree from re-rendering. If CartBadge uses context but wraps its children in memo, the children are protected even when the badge re-renders.Follow-up: Some developers avoid Context for frequently changing values (like a real-time cursor position) and use Zustand or Jotai instead. Why?Context re-renders ALL consumers on every value change, and there is no granular subscription mechanism. For a cursor position updating at 60fps, every component in the context tree re-renders 60 times per second. Even with React.memo on most children, the consumers themselves still re-render and run their function bodies.Zustand and Jotai use external stores with selector-based subscriptions. A component subscribes to a specific slice of state: useStore(state => state.cursor). When cursor changes, only components that selected cursor re-render. Components that selected state.theme are completely untouched. This is possible because these libraries bypass React’s context mechanism and use useSyncExternalStore (React 18) to trigger granular re-renders.The practical threshold: if context value changes more than a few times per second or has many consumers (50+), switch to an external store. For infrequently changing values like theme, locale, and auth status, Context is perfectly adequate and simpler.Walk me through the 'Provider hell' problem and how you solve it in a large application with many contexts.
Walk me through the 'Provider hell' problem and how you solve it in a large application with many contexts.
Strong Answer:
Provider hell is when your root component has 10+ nested Providers, each wrapping the previous one. It looks like a pyramid of doom in JSX and makes the component tree hard to reason about in DevTools. The order of providers can matter (an AuthProvider that depends on an ApiProvider must be nested inside it), which creates implicit dependency chains.The simplest fix is a This is cleaner but does not solve the underlying question: should you have this many contexts?For large applications, I evaluate each context: does it need to be global? Auth and theme are truly global. A shopping cart might be scoped to the e-commerce section. Form state should be local. Moving contexts to the lowest common ancestor reduces the blast radius of re-renders and makes the app structure clearer.Another approach: consolidate related contexts using useReducer. Instead of separate This pattern (which React Testing Library’s docs recommend) lets each test specify only the overrides it cares about. For unit tests of a component that uses
ComposeProviders utility component that takes an array of providers and nests them automatically:UserContext, PreferencesContext, and NotificationsContext, combine them into a single AppContext with a reducer that handles all three concerns. Split the state and dispatch into separate contexts for performance. This reduces Provider count while maintaining granular subscriptions.Follow-up: How do you test components that depend on multiple contexts?Create a test utility function that wraps components with the necessary providers pre-configured with test-friendly defaults:useAuth, you can mock the hook directly: vi.mock('./useAuth', () => ({ useAuth: () => ({ user: testUser }) })). This avoids setting up the full provider tree when you only need one context value.For integration tests, use the real providers with controlled initial state. This tests that the context wiring actually works end-to-end, catching bugs like a missing Provider or wrong context reference.Compare Context API with useReducer versus Redux Toolkit for managing global state. When does Context stop being sufficient?
Compare Context API with useReducer versus Redux Toolkit for managing global state. When does Context stop being sufficient?
Strong Answer:
Context plus useReducer is essentially a lightweight, built-in Redux. You get a central store (context value), actions (dispatched objects), a reducer (pure function), and subscriptions (useContext consumers). For small to medium apps, this is often all you need.Context stops being sufficient at three thresholds. First, performance: when you have many consumers and frequent state updates, Context’s all-or-nothing re-render behavior becomes a bottleneck. Redux’s
useSelector with shallow equality checking only re-renders components whose selected slice changed. For a dashboard with 50 widgets reading different parts of global state, Redux prevents 49 unnecessary re-renders that Context would trigger.Second, middleware and side effects: Redux Toolkit’s createAsyncThunk and middleware pipeline give you a structured way to handle async logic, logging, error reporting, and optimistic updates. With Context, you manually implement all of this in your Provider or custom hooks. It is doable but messy at scale.Third, developer tools: Redux DevTools offer time-travel debugging, action replay, state diffs, and export/import of state snapshots. This is invaluable for debugging complex state transitions in production. Context has no equivalent tooling.My decision framework: start with local state. When multiple components need shared state, use Context. When you hit performance issues with Context or need more than 5-6 actions with complex async logic, evaluate Redux Toolkit or Zustand. For server state specifically (API data with caching, pagination, optimistic updates), skip Redux entirely and use React Query.Follow-up: Zustand has gained popularity over Redux for many teams. What makes it different and when would you still choose Redux?Zustand is dramatically simpler: no Providers, no wrapping your app, no actions/reducers pattern. You create a store with create(), define state and updaters as a plain object, and consume with a hook that supports selectors. The entire API is 5 functions. Bundle size is around 1KB.I would still choose Redux Toolkit when: the team is large and needs enforced patterns (actions, reducers, slices give structure that Zustand does not impose), when you need the DevTools time-travel debugging for complex state machines, when you have significant async logic that benefits from createAsyncThunk and middleware, or when the project already uses Redux and migration cost outweighs Zustand’s simplicity.I would choose Zustand when: the team is small, state logic is straightforward, bundle size matters (Zustand is 10x smaller than Redux Toolkit), and you value API simplicity over structural patterns. Zustand is particularly good for state that is not connected to React at all (like a game engine or a shared state between React and vanilla JS).