useContext useReducer—Ghost Cart: Mutation Skips Re-render
After clearing the cart, total price lingered because reducer's sort() mutated the array.
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- useContext creates a subscription channel from Provider to any descendant component, eliminating prop drilling.
- useReducer centralizes state transition logic into a pure function (the reducer) for complex flows.
- Separating state and dispatch contexts prevents re-renders: dispatch-only components skip state updates.
- Performance: each context update triggers re-render in all consumers — keep values stable or split contexts.
- Production insight: a single giant AppContext causes cascading re-renders; split by domain (auth, cart, UI).
- Biggest mistake: mutating state inside the reducer instead of returning new objects — React won't re-render.
Imagine your school has a PA system. Instead of a teacher running to every classroom to announce lunch is ready, one announcement goes out and every room hears it instantly. That's useContext — shared information without passing notes room to room. Now imagine the school office has a strict rule book: if a student is absent, a specific form gets filled out; if there's a fire drill, a specific procedure runs. That's useReducer — predictable, rule-based responses to events. Together, they're your school's entire communication and decision-making system, built right into React.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
React’s Context API and useReducer are often cargo-culted into a makeshift state manager, but that misconception misses the point. UseContext solves prop drilling, not global state performance. UseReducer simplifies complex state logic when useState starts spawning bugs. This article breaks down exactly what each tool does, where they fail, and how to combine them safely—without burning your app to the ground.
Why useContext + useReducer Is Not a State Manager
useContext + useReducer is a React pattern that combines context for dependency injection with a reducer for state transitions. The core mechanic: useReducer returns a dispatch function that you pass through context, allowing deeply nested components to trigger state updates without prop drilling. But here's the catch—React's bailout logic means that if a reducer returns the same state reference (e.g., mutating an array or object in place), the context value doesn't change, and no subscriber re-renders. This is not a bug; it's React's referential equality check for performance. In practice, you must always return a new object or array from the reducer. The pattern works well for medium-complexity state that doesn't need middleware, devtools, or cross-component caching. It fails when you treat it like Redux—it lacks batching guarantees, selector memoization, and external store subscriptions. Use it for form state, wizard steps, or UI toggles; avoid it for server cache or real-time data.
Why Prop Drilling Is a Real Problem (and What useContext Actually Solves)
Prop drilling isn't just annoying — it's a maintenance hazard. When a component in the middle of your tree needs to pass a prop purely so its child's child can use it, that middle component becomes coupled to data it doesn't care about. Rename a prop, change its shape, or remove it, and you're hunting down every intermediary component that passed it along.
useContext solves this by creating a named 'channel' in your app. A Provider component sits near the top of your tree and broadcasts a value. Any component anywhere below it can tune into that channel directly using the useContext hook. The components in between don't need to know it exists.
This is perfect for genuinely global data: authentication state, user preferences, UI themes, or locale settings. The key word is global. useContext isn't a replacement for all props — if two sibling components share local state, lifting state up is still the right move. Context is for data that many, structurally distant components need simultaneously.
Think of createContext as setting up the PA system, the Provider as turning the microphone on, and useContext as each classroom having a speaker.
Provider/Consumer Data Flow: How Context Reaches Every Component
Visualising the Provider/Consumer relationship helps you debug subscription issues and plan your context architecture. The diagram below shows a typical setup where a ThemeProvider wraps the entire app tree, and two deeply nested consumers directly access the theme without any props passing through intermediate components.
The key insight: context flows downward from the Provider, but consumers can be at any depth. Each consumer subscribes individually — React uses the component tree to find the nearest matching Provider ancestor. If no Provider exists, the default value passed to createContext is used (or undefined if not provided).
useReducer: When useState Gets Too Complicated to Trust
useState is perfect for simple, independent values. But the moment your state transitions get conditional — 'if the cart has this item, increment its quantity; otherwise, add it; but if quantity hits zero, remove it entirely' — useState starts to crack. You end up with sprawling logic scattered across event handlers, and it becomes hard to trace exactly how your state got into a particular shape.
useReducer pulls all that logic into one pure function called the reducer. A reducer takes the current state and an action object, and returns the new state. That's it. No side effects, no API calls, no DOM mutations — just 'given THIS state and THIS action, HERE is the next state.'
This pattern comes directly from the Flux/Redux world, but useReducer bakes it into React with no extra libraries. The mental model is a vending machine: you press a button (dispatch an action), the machine's internal logic (the reducer) decides what happens, and you get a result. You don't reach inside the machine — you use the defined interface.
This makes your state transitions auditable. You can log every action, write unit tests against your reducer in complete isolation from React, and reason about state changes without understanding the full component tree.
Date.now(), no Math.random().Migration Guide: From useState to useReducer (Step by Step)
Moving from a local useState that has grown unwieldy to a clean useReducer pattern is straightforward. Follow these steps and you'll have a testable, centralized state logic in under an hour.
Step 1: Identify the state — Look for a useState call that manages an object or an array with multiple related fields, especially if you have complex update logic in event handlers.
Step 2: Define action types — Create an object with string constants for every way state can change (e.g., ADD_ITEM, REMOVE_ITEM, CLEAR). This prevents typos and makes the possible transitions easy to review.
Step 3: Extract the reducer function — Write a pure function that takes (state, action) and returns new state. Use a switch on action.type. Keep it in its own file for testability.
Step 4: Replace useState with useReducer — Swap const [state, setState] = useState(initial) to const [state, dispatch] = useReducer(reducer, initialState).
Step 5: Update event handlers — Instead of calling something like setState(prev => ...), each handler now calls dispatch({ type: 'ACTION_NAME', payload: ... }).
Optional Step: Make it context-ready — If multiple components need the same state, wrap the useReducer in a Provider and expose state + dispatch via contexts as shown earlier.
no-param-reassign ESLint rule (from eslint-plugin-import) and set it to error inside reducer files. This prevents accidental .push(), .pop(), .sort() on arguments. The rule react/no-direct-mutation-state catches mutations in useState calls but doesn't cover useReducer; the no-param-reassign rule fills that gap.setState calls after switching to dispatch, creating two competing sources of truth. During migration, remove the old useState entirely before introducing useReducer to avoid confusing bugs.State Management Comparison: Which Tool for Which Job?
Choosing the right state management approach is a critical architectural decision. The table below compares the five most popular patterns in React today across dimensions that matter in production: setup complexity, scope, performance, testability, and team scalability.
| Aspect | useState (local) | useReducer (local) | useContext + useReducer | Zustand | Redux Toolkit |
|---|---|---|---|---|---|
| Setup complexity | Zero — built in | Zero — built in | Low — two hooks, one context file | Low — create store, no Provider needed | Medium — store, slices, Provider |
| State scope | Single component | Single component | Component subtree (feature-level) | Global but can scope | Entire application |
| Logic complexity | Simple values only | Complex transitions, multiple cases | Complex transitions, multiple cases | Workflow-based, flexible | Complex async, middleware, side effects |
| Performance risk | None | None | Re-renders all consumers on context change | Very low — no context re-render cascade | Minimal — optimised selectors built in |
| DevTools support | React DevTools only | React DevTools only | React DevTools only | Zustand DevTools (basic) | Redux DevTools with time travel |
| Testability of logic | Logic is in component | Reducer is pure isolated function | Reducer is pure isolated function | Store logic is isolated | Slices are pure isolated functions |
| Best for | Modal open/close, form fields | Single-component complex state | Cart, auth session, multi-step forms | Mid-size apps, frequent updates | Large teams, complex async workflows |
| Bundle size added | 0 bytes | 0 bytes | 0 bytes | ~2KB minified | ~12KB (RTK minified) |
Key takeaway: No single tool is the best. The decision should be based on the specific pain points of your app. For teams starting out, go with useState → useContext+useReducer — you can always graduate to Zustand or Redux Toolkit later without rewriting everything.
useStore hook that directly subscribes to a store with a selector. Only components that select the changed slice re-render — no cascading. This is why Zustand is often recommended for high-frequency updates that would be problematic in plain useContext.Combining useContext + useReducer: Building a Real Shopping Cart
This is where the two hooks become genuinely powerful. useReducer manages the HOW of state changes — all that complex cart logic lives in one pure, testable function. useContext handles the WHERE — making that state and the dispatch function available to any component without prop drilling.
The pattern is always the same: create a context, build a Provider that runs useReducer internally, and expose both the state and dispatch through that context. Consumer components get access to exactly what they need.
One critical detail: expose dispatch directly rather than wrapping every possible action in a separate callback function. Some tutorials create addToCart, removeFromCart, clearCart as individual functions in context — but this means updating your Provider every time you add a new action type. Exposing dispatch directly means consumers can send any action the reducer understands, and your Provider never needs to change.
This is the architecture pattern you'll see in production codebases. It separates concerns cleanly: the reducer owns business logic, the Provider owns state lifecycle, and consumer components own UI rendering.
When NOT to Use useContext Alone (Performance Pitfalls)
While useContext itself is a simple hook, using it incorrectly can tank your app's performance. Here are the key scenarios where you should avoid useContext — or restructure how you use it.
1. Frequently Changing Values — If you put a value in context that updates multiple times per second (mouse position, animation frame, live search results), every subscribed component will re-render on every change. This can cause jank, especially on lower-end devices. Keep such values in local useState or use refs.
2. Object/Array Values Without Memoisation — If your provider creates a new object or array on every render (e.g., value={{ user, setUser }}), all consumers re-render even if the data didn't logically change. Always wrap the context value in useMemo if it's an object or array.
3. Deeply Nested Providers — Having many nested providers (e.g., ThemeProvider > AuthProvider > CartProvider > UIConfigProvider) can slow down tree reconciliation. React walks the tree to find context values, and deeply nesting slows this walk. Flatten where possible.
4. Misuse as Global State for Business Logic — Using useContext for state that should live in a client-side database (like cached API responses) leads to performance issues because context doesn't provide memoisation or efficient selectors like Redux or Zustand do.
5. Testing Complexity — Components that rely on context become harder to unit test because you must wrap them in the appropriate provider(s) even for simple renders. Often, prop drilling is simpler for depth-1 or depth-2 data.
useMemo for context values that are objects or arrays, and consider using the split-context pattern to allow components to subscribe only to the parts they need.When NOT to Use This Pattern (and What to Use Instead)
Context + useReducer is genuinely powerful, but it's not the right tool for every job. Knowing when NOT to use it is what separates good engineers from great ones.
Context re-renders every subscriber whenever its value changes. If you put your entire application state in a single context and update it frequently — say, a real-time feed that changes every second — every component that reads that context re-renders on every update. This is why the split-context pattern from the previous section matters, and why very high-frequency state (animation frames, mouse position, live typing) should stay in local useState, not context.
For large-scale apps with complex async flows, multiple developers, time-travel debugging needs, or middleware requirements, Redux Toolkit or Zustand are better choices. They add structure that a growing team needs, and tools like Redux DevTools are genuinely irreplaceable when debugging complex state bugs.
The right mental model: useContext + useReducer is a great fit for feature-scoped state (an entire checkout flow, a wizard form, a dashboard filter system) shared across a component subtree. It's less suited for truly app-wide state that dozens of components all need simultaneously at high update frequency.
Testing the Pattern in Isolation: Reducer Unit Tests and Context Integration Tests
One of the biggest advantages of useReducer is that the reducer is a pure function — you can test it without mounting any React components. That makes your state logic fast to verify and resistant to UI regressions. For the provider and consumer behaviour, integration tests with React Testing Library fill the gap.
Every reducer should be tested on its own: feed it an initial state and an action, assert the returned state matches expectations. This catches edge cases like zero-quantity items being removed, totals recalculating correctly, and unknown actions returning current state.
For context integration, render the provider with a test consumer that reads state and dispatches actions. Verify the consumer re-renders with the correct values after dispatch. This tests that the wiring between context, dispatch, and the component is correct.
Key pitfalls: avoid testing internal implementation details (e.g., checking that a specific reducer case was called). Instead, test the visible outcome — what does the component render after a sequence of actions?
How Context + useReducer Bypasses the Prop Drilling Tax Without a Library
You've got a modal that opens from a button three levels deep. Or a theme toggle that every component needs to read. Prop drilling means threading a callback through every intermediate component that doesn't care about it. That's noise. That's maintenance debt.
useContext gives any nested component direct access to a value without passing it through every parent. Combine it with useReducer and you're not just reading state — you're dispatching actions from anywhere in the tree. The component that needs to open a modal calls dispatch({ type: 'OPEN_MODAL' }). The component three levels up listens for the state change. No libraries, no new dependencies, no ceremony.
This isn't about replacing Redux. It's about solving a specific problem: local state that needs to live above the component that uses it, without polluting every intermediate component with props it never touches.
The Reducer Pattern That Scales Your Logic Without Scaling Your Bugs
A complex form with interdependent fields. A multi-step checkout with undo. A real-time dashboard with optimistic updates. useState in these scenarios turns into a swamp of race conditions, stale closures, and duplicated logic. You've been there. You've debugged it at 2 AM.
useReducer forces you to define every possible state transition as a pure function. That's the point. No side effects, no hidden state mutations. You dispatch an action, the reducer returns the next state. Predictable. Testable. You can unit test the reducer in isolation without mounting a single component.
The win here isn't just fewer bugs. It's that the logic lives in one place. Need to change how a field validation works? You edit the reducer. Need to add a new action? You add a case. The component just reads state and dispatches events. Separation of concerns with a knife edge.
Why Your Components Should Be Dumb and Your Reducers Smart
Look at your typical React component. It fetches data, manipulates state, handles UI events, and sometimes tries to do its own validation. That's the opposite of separation of concerns. That's a maintenance nightmare waiting for the next feature request.
With useReducer, you make components dumb. They read state from context, render it, and dispatch actions. That's it. The reducer owns all the logic — state transitions, validation, conditional flows. Your delete button doesn't know how to delete. It just dispatches { type: 'DELETE_ITEM' }. The reducer decides whether to optimistically remove the item, show a loading state, or roll back on error.
This patterns lets you change business logic without touching UI. New validation rule? Add it to the reducer. New UI framework? Your reducers are pure functions — they don't care about your JSX. You build a system that's resilient to change exactly where change happens most: business requirements.
Github Repo: Stop Rewriting the Same Cart — Fork This
Every team builds the same useContext + useReducer shopping cart pattern. Waste of cycles. Instead of treating each project like a snowflake, grab a production-hardened starter that ships with typed reducers, context providers already wrapped, and a test harness.
The repo below isn't a tutorial. It's a battle kit. Clone it, rip out the ProductList, plug in your API, and your cart state is battle-ready in 10 minutes. The reducer handles add, remove, clear, and bulk update. Integration tests prove the context dispatches correctly without mounting the whole DOM. Unit tests validate every reducer branch — no more guessing "did I handle the edge case where quantity hits zero?"
Stop cargo-culting. One git clone, and you're shipping state logic that senior engineers trust in code review. The only thing you customize is the data shape. Everything else is vetted.
Explanation
useContext passes a value object down the component tree. useReducer creates a state container and a dispatch function that triggers reducer logic. Combined, they give you centralized state with predictable updates — no Redux required. The reducer owns all mutation logic; components only call dispatch with a plain action object. This decouples UI from state transitions. The context provider wraps your tree, making dispatch and state available anywhere without prop threading. Every dispatch runs the reducer synchronously, producing the next state. React re-renders only the consumers of that context. This pattern works because the reducer is a pure function: same action + same state = same result. Predictable, testable, and easy to reason about. No subscriptions, no global singletons — just explicit data flow through React's own primitives. The explanation starts with the problem (shared mutable state across many components) and shows how these two hooks solve it without a library.
Recap and Conclusion
UseContext gives you subscription-based state sharing without prop drilling. useReducer gives you predictable, testable state transitions. Together they replace small-to-medium state management needs without pulling in a library. The pattern breaks when performance matters — every context update re-renders all consumers. Split your contexts. Keep reducers pure. Never put derived data in state — compute it on the fly. If your reducer logic grows beyond 50 lines or you need middleware, consider Zustand or Redux Toolkit instead. This pattern solves the prop drilling tax, but it's not a scalable store. Use it for feature-local state that multiple components need. For global app state, reach for a real state manager. The final takeaway: useContext + useReducer gives you Redux-like discipline with zero dependencies. Start here, scale up when you hit hard walls.
Split State and Dispatch Contexts for Performance
When using useContext with useReducer, every consumer of the context re-renders whenever the state changes, even if they only need the dispatch function. This causes unnecessary re-renders in components that never read state. To avoid this, split the context into two separate contexts: one for state and one for dispatch. Components that only call dispatch (e.g., buttons, input handlers) subscribe only to the dispatch context, which never changes, so they never re-render. This pattern is critical for performance in large applications.
Implementation: - Create StateContext and DispatchContext. - In the provider, pass state to StateContext.Provider and dispatch to DispatchContext.Provider. - Use useContext(StateContext) in components that read state. - Use useContext(DispatchContext) in components that only dispatch actions.
Example: ```jsx const StateContext = createContext(); const DispatchContext = createContext();
function CartProvider({ children }) { const [state, dispatch] = useReducer(cartReducer, initialState); return ( <DispatchContext.Provider value={dispatch}> <StateContext.Provider value={state}> {children} </StateContext.Provider> </DispatchContext.Provider> ); }
function useCartState() { return useContext(StateContext); }
function useCartDispatch() { return useContext(DispatchContext); } `` Now, a component that only adds items to the cart can use useCartDispatch()` without re-rendering when the cart changes.
Multi-Context Provider Tree Architecture
Real-world applications often need multiple contexts for different domains (e.g., authentication, theming, UI state). Instead of nesting providers deeply, organize them in a clear provider tree. Each context should be independent and only wrap the parts of the tree that need it. This avoids unnecessary re-renders and keeps the architecture modular.
Example structure: ``jsx function AppProviders({ children }) { return ( ``
Best practices: - Keep contexts small and focused (e.g., AuthContext for user data, ThemeContext for theme, UIContext for modals/sidebars). - Use separate providers for state and dispatch (as described above) for each context if performance is a concern. - Avoid putting everything in one global context; it defeats the purpose of modularity.
Example with AuthContext and ThemeContext: ```jsx const AuthContext = createContext(); const ThemeContext = createContext();
function AuthProvider({ children }) { const [user, dispatch] = useReducer(authReducer, null); return (
function ThemeProvider({ children }) { const [theme, dispatch] = useReducer(themeReducer, 'light'); return (
This architecture scales well and makes it easy to test each context in isolation.
Immer with useReducer
Writing immutable updates in reducers can be verbose and error-prone, especially with nested objects. Immer simplifies this by allowing you to write mutable-style code that produces immutable state. By using Immer's produce function inside your reducer, you can directly mutate a draft state, and Immer will handle the immutable update for you.
Installation: npm install immer
Example without Immer: ``js function cartReducer(state, action) { switch (action.type) { case 'ADD_ITEM': return { ...state, items: [...state.items, action.payload] }; // ... more cases } } ``
With Immer: ```js import { produce } from 'immer';
const cartReducer = produce((draft, action) => { switch (action.type) { case 'ADD_ITEM': draft.items.push(action.payload); break; case 'REMOVE_ITEM': draft.items = draft.items.filter(item => item.id !== action.payload.id); break; case 'UPDATE_QUANTITY': const item = draft.items.find(i => i.id === action.payload.id); if (item) item.quantity = action.payload.quantity; break; } }); ```
Immer's produce returns a new state automatically. The reducer becomes more readable and less prone to bugs. This is especially useful for deeply nested state like a shopping cart with multiple items and properties.
React 19 use(Context) Hook
React 19 introduces a new use hook that can be used to read context values conditionally, even inside loops or early returns. Unlike useContext, which must be called at the top level of a component, use can be called anywhere, making it more flexible. This is particularly useful when you need to conditionally read context based on props or other state.
Syntax: ```jsx import { use } from 'react';
function MyComponent({ context }) { if (someCondition) { const value = use(context); // ... } return null; } ```
Example with conditional reading: ``jsx function CartItem({ item, showDiscount }) { if (showDiscount) { const discount = use(DiscountContext); return ``
This avoids the need to always read the context and can improve performance by skipping context subscription when not needed. However, use is still experimental in React 19; use with caution in production.
Comparison with useContext: - useContext must be called at the top level of a component. - use can be called conditionally, inside loops, or after early returns. - use returns the same value as useContext for the same context.
Note: The use hook is not a replacement for useContext in all cases; it's an alternative for scenarios where conditional reading is beneficial.
use sparingly in production until it's stable. It's best for cases where context is only needed in certain branches of a component.use hook enables conditional context reading, offering more flexibility than useContext for performance-sensitive scenarios.useContext+useReducer vs Redux/Zustand Decision Guide
Choosing between useContext+useReducer and a dedicated state management library like Redux or Zustand depends on your app's complexity and requirements. Here's a comparison to help decide.
Comparison Table: | Feature | useContext+useReducer | Redux | Zustand | |---------|----------------------|-------|---------| | Boilerplate | Low | High | Low | | DevTools | None built-in | Excellent | Good (with extension) | | Middleware | None | Thunk, Saga, etc. | Built-in (subscribe) | | Performance | Manual optimization needed | Optimized with selectors | Optimized with selectors | | Scalability | Good for small-medium apps | Excellent for large apps | Good for all sizes | | Learning curve | Low | High | Low | | Bundle size | ~0 KB (built-in) | ~11 KB min+gzip | ~2 KB min+gzip |
When to use useContext+useReducer: - Small to medium apps with simple state. - You want to avoid external dependencies. - State is mostly local to a feature or subtree.
When to use Redux: - Large apps with complex state interactions. - Need for robust middleware (e.g., async actions). - Team familiarity with Redux patterns.
When to use Zustand: - You want a lightweight, simple API. - Need for performance with minimal boilerplate. - Prefer a store-based approach without context.
Decision Flowchart: 1. Is your app small or medium? → useContext+useReducer 2. Do you need middleware or dev tools? → Redux 3. Want minimal boilerplate and good performance? → Zustand
Example: For a shopping cart in a small e-commerce site, useContext+useReducer is sufficient. For a large dashboard with many interconnected states, Redux or Zustand may be better.
The Ghost Cart: Why Clearing the Cart Didn't Clear the UI
return { ...state, items: [] } and assumed that would work — but the totalPrice was derived from items.reduce(...) inside the same return, and the reducer was accidentally mutating the original array before returning.calculateTotals(items) that internally mutated the items array by sorting it (using items.sort() mutates in place). The reducer then spread the totals into a new state object, but the items reference was already mutated. Because the mutation happened before the new object was created, React's shallow comparison saw the same array reference and skipped the re-render for components reading items.items.sort() with [...items].sort() inside the helper to create a copy before sorting. Also add a lint rule: no-param-reassign for reducer files.- Pure reducers must create new objects and arrays — never mutate inputs.
- Eslint plugin-react-hooks and plugin-import catch common mutation patterns.
- Unit-test the reducer with deep equality assertions on every action.
- Derive totals inside the component using useMemo to decouple state shape from UI calculations.
JSON.stringify(prevState) === JSON.stringify(nextState) to detect accidental equality. If reducer mutates input (e.g., items.push()), the reference stays the same.useContext(CartStateContext). Also verify the Provider is not nested inside a conditional or another context that might not render. Use React DevTools to inspect the component tree and confirm the Provider exists above the consumer.useMemo if values are objects or arrays.case in the reducer. Typo in action type (e.g., 'ADD' vs 'ADD_ITEM') will hit default and return current state. Define action types as constants to prevent this.Add `console.log('state:', state, 'action:', action)` as first line of reducer.In browser console: right-click on rendered state → 'Store as global variable' to inspect.return { ...state, items: [...state.items, newItem] }.| File | Command / Code | Purpose |
|---|---|---|
| ThemeContext.jsx | const ThemeContext = createContext('light'); | Why Prop Drilling Is a Real Problem (and What useContext Act |
| cartReducer.js | export const CART_ACTIONS = { | useReducer |
| MigrationExample.jsx | function ShoppingCartBefore() { | Migration Guide |
| CartFeature.jsx | const CartStateContext = createContext(null); // holds the state | Combining useContext + useReducer |
| ContextPerformanceAntiPattern.jsx | function BadProvider({ children }) { | When NOT to Use useContext Alone (Performance Pitfalls) |
| WhenToChoose.jsx | const [isMenuOpen, setIsMenuOpen] = useState(false); | When NOT to Use This Pattern (and What to Use Instead) |
| cartReducer.test.js | describe('cartReducer', () => { | Testing the Pattern in Isolation |
| ModalProvider.js | const ModalContext = createContext(null); | How Context + useReducer Bypasses the Prop Drilling Tax With |
| CheckoutReducer.js | const initialState = { | The Reducer Pattern That Scales Your Logic Without Scaling Y |
| ShoppingCart.js | function CartItem({ id, name, price, quantity }) { | Why Your Components Should Be Dumb and Your Reducers Smart |
| CartReducer.js | const initialState = { items: [], total: 0 } | Github Repo: Stop Rewriting the Same Cart |
| ExplainingThePattern.js | const CountContext = createContext(null); | Explanation |
| RecapExample.js | const CartContext = createContext(null); | Recap and Conclusion |
| SplitContexts.jsx | const StateContext = createContext(); | Split State and Dispatch Contexts for Performance |
| MultiContextProviders.jsx | function AppProviders({ children }) { | Multi-Context Provider Tree Architecture |
| immerReducer.js | const cartReducer = produce((draft, action) => { | Immer with useReducer |
| useContextHook.jsx | function CartItem({ item, showDiscount }) { | React 19 use(Context) Hook |
Key takeaways
Interview Questions on This Topic
What's the difference between useContext and prop drilling, and when would you choose useContext over simply lifting state up?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
That's React. Mark it forged?
15 min read · try the examples if you haven't