javascript
65 lines · 10 steps
Building a shopping cart with Context in React
A reducer holds cart state while Context and a custom hook share it across the component tree.
Explained by
highlit
1import { createContext, useContext, useReducer } from 'react';
2
3const CartContext = createContext(null);
4
5function cartReducer(state, action) {
6 switch (action.type) {
7 case 'ADD_ITEM': {
8 const existing = state.items.find((i) => i.id === action.product.id);
9 if (existing) {
10 return {
11 items: state.items.map((i) =>
12 i.id === action.product.id
13 ? { ...i, quantity: i.quantity + 1 }
14 : i
15 ),
16 };
17 }
18 return {
19 items: [...state.items, { ...action.product, quantity: 1 }],
20 };
21 }
22 case 'REMOVE_ITEM':
23 return {
24 items: state.items.filter((i) => i.id !== action.id),
25 };
26 case 'SET_QUANTITY': {
27 if (action.quantity <= 0) {
28 return { items: state.items.filter((i) => i.id !== action.id) };
29 }
30 return {
31 items: state.items.map((i) =>
32 i.id === action.id ? { ...i, quantity: action.quantity } : i
33 ),
34 };
35 }
36 case 'CLEAR':
37 return { items: [] };
38 default:
39 return state;
40 }
41}
42
43export function CartProvider({ children }) {
44 const [state, dispatch] = useReducer(cartReducer, { items: [] });
45
46 const value = {
47 items: state.items,
48 total: state.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
49 addItem: (product) => dispatch({ type: 'ADD_ITEM', product }),
50 removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', id }),
51 setQuantity: (id, quantity) =>
52 dispatch({ type: 'SET_QUANTITY', id, quantity }),
53 clear: () => dispatch({ type: 'CLEAR' }),
54 };
55
56 return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
57}
58
59export function useCart() {
60 const context = useContext(CartContext);
61 if (!context) {
62 throw new Error('useCart must be used within a CartProvider');
63 }
64 return context;
65}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A reducer centralizes all state transitions so every mutation flows through one predictable switch statement.
- 2Returning fresh objects and arrays instead of mutating keeps React's change detection reliable.
- 3Wrapping useContext in a guarded custom hook enforces correct usage and gives consumers a clean API.
Related explainers
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
javascript
const express = require('express'); const router = express.Router(); const { pool } = require('../db'); const redis = require('../redis');
Building a health check endpoint in Express
health-check
timeouts
promise-race
Intermediate
9 steps
javascript
'use client'; import { useEffect } from 'react'; import * as Sentry from '@sentry/nextjs';
How a Next.js error boundary recovers
error-boundary
error-handling
observability
Intermediate
8 steps
java
public record FixedWidthField(String name, int start, int end) { public String extract(String line) { int from = Math.min(start, line.length());
Parsing fixed-width text in Java
records
parsing
streams
Intermediate
7 steps
javascript
const MAX_BATCH_SIZE = 20; const FLUSH_INTERVAL = 5000; const ENDPOINT = '/api/analytics';
Batching analytics events in the browser
batching
buffering
sendbeacon
Intermediate
10 steps
javascript
export function diffIds(current, desired) { const currentSet = new Set(current); const desiredSet = new Set(desired);
Diffing sets to sync group membership
set-operations
diffing
transactions
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-shopping-cart-with-context-in-react-explained-javascript-2839/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.