Refactor Part 2: Co-locate State and Use Multiple Contexts
In many real apps, the biggest win comes from not putting everything in a single context at all. Instead:
Local UI state stays local (isToppingsPanelOpen, isSizePopoverOpen, etc.).
Shared state is split into smaller contexts per concern.
For our Pizza Builder, a reasonable split could be:
PizzaConfigContext for the actual pizza choice (size, base, toppings).
PricingContext (or just a hook) for derived pricing logic.
Local state inside components for any purely visual toggles.
Here’s an example where size/base and toppings are separate contexts. This is a bit more verbose, but demonstrates the pattern.
1
2// src/context/PizzaSizeBaseContext.tsx
3import React, {
4 createContext,
5 useContext,
6 useState,
7 ReactNode,
8} from 'react';
9
10type PizzaSize = 'small' | 'medium' | 'large';
11type PizzaBase = 'thin' | 'regular' | 'deep-dish';
12
13type SizeBaseState = {
14 size: PizzaSize | null;
15 base: PizzaBase | null;
16};
17
18type SizeBaseContextValue = {
19 state: SizeBaseState;
20 setSize: (size: PizzaSize) => void;
21 setBase: (base: PizzaBase) => void;
22};
23
24const SizeBaseContext = createContext<SizeBaseContextValue | undefined>(
25 undefined
26);
27
28export function SizeBaseProvider({ children }: { children: ReactNode }) {
29 const [state, setState] = useState<SizeBaseState>({
30 size: null,
31 base: null,
32 });
33
34 const setSize = (size: PizzaSize) => {
35 setState(prev => ({ ...prev, size }));
36 };
37
38 const setBase = (base: PizzaBase) => {
39 setState(prev => ({ ...prev, base }));
40 };
41
42 return (
43 <SizeBaseContext.Provider value={{ state, setSize, setBase }}>
44 {children}
45 </SizeBaseContext.Provider>
46 );
47}
48
49export function useSizeBase() {
50 const ctx = useContext(SizeBaseContext);
51 if (!ctx) {
52 throw new Error('useSizeBase must be used within SizeBaseProvider');
53 }
54 return ctx;
55}
561// src/context/PizzaToppingsContext.tsx
2import React, {
3 createContext,
4 useContext,
5 useState,
6 ReactNode,
7} from 'react';
8
9type ToppingId =
10 | 'pepperoni'
11 | 'mushrooms'
12 | 'onions'
13 | 'olives'
14 | 'extra-cheese';
15
16type ToppingsContextValue = {
17 toppings: ToppingId[];
18 toggleTopping: (topping: ToppingId) => void;
19 resetToppings: () => void;
20};
21
22const ToppingsContext = createContext<ToppingsContextValue | undefined>(
23 undefined
24);
25
26export function ToppingsProvider({ children }: { children: ReactNode }) {
27 const [toppings, setToppings] = useState<ToppingId[]>([]);
28
29 const toggleTopping = (topping: ToppingId) => {
30 setToppings(prev => {
31 const hasTopping = prev.includes(topping);
32 return hasTopping
33 ? prev.filter(t => t !== topping)
34 : [...prev, topping];
35 });
36 };
37
38 const resetToppings = () => setToppings([]);
39
40 return (
41 <ToppingsContext.Provider
42 value={{ toppings, toggleTopping, resetToppings }}
43 >
44 {children}
45 </ToppingsContext.Provider>
46 );
47}
48
49export function useToppings() {
50 const ctx = useContext(ToppingsContext);
51 if (!ctx) {
52 throw new Error('useToppings must be used within ToppingsProvider');
53 }
54 return ctx;
55}
56Now the page composition becomes:
1// src/pages/pizza-builder-optimized.tsx
2import React from 'react';
3import { SizeBaseProvider } from '../context/PizzaSizeBaseContext';
4import { ToppingsProvider } from '../context/PizzaToppingsContext';
5import { PizzaSizeSelector } from '../components/PizzaSizeSelectorOptimized';
6import { ToppingsSelector } from '../components/ToppingsSelectorOptimized';
7import { OrderSummaryOptimized } from '../components/OrderSummaryOptimized';
8
9export default function PizzaBuilderOptimizedPage() {
10 return (
11 <SizeBaseProvider>
12 <ToppingsProvider>
13 <main>
14 <h1>Build your pizza (optimized)</h1>
15 <PizzaSizeSelector />
16 <ToppingsSelector />
17 <OrderSummaryOptimized />
18 </main>
19 </ToppingsProvider>
20 </SizeBaseProvider>
21 );
22}
23And consumers:
1// src/components/PizzaSizeSelectorOptimized.tsx
2import React from 'react';
3import { useSizeBase } from '../context/PizzaSizeBaseContext';
4
5export function PizzaSizeSelector() {
6 const { state, setSize } = useSizeBase();
7 const { size } = state;
8
9 return (
10 <section>
11 <h2>Choose size</h2>
12 {(['small', 'medium', 'large'] as const).map(s => (
13 <button
14 key={s}
15 onClick={() => setSize(s)}
16 aria-pressed={size === s}
17 >
18 {s}
19 </button>
20 ))}
21 </section>
22 );
23}
241// src/components/ToppingsSelectorOptimized.tsx
2import React from 'react';
3import { useToppings } from '../context/PizzaToppingsContext';
4
5const allToppings = [
6 { id: 'pepperoni', label: 'Pepperoni' },
7 { id: 'mushrooms', label: 'Mushrooms' },
8 { id: 'onions', label: 'Onions' },
9 { id: 'olives', label: 'Olives' },
10 { id: 'extra-cheese', label: 'Extra Cheese' },
11] as const;
12
13export function ToppingsSelector() {
14 const { toppings, toggleTopping } = useToppings();
15
16 return (
17 <section>
18 <h2>Choose toppings</h2>
19 {allToppings.map(topping => {
20 const selected = toppings.includes(topping.id);
21 return (
22 <button
23 key={topping.id}
24 onClick={() => toggleTopping(topping.id)}
25 aria-pressed={selected}
26 >
27 {topping.label} {selected ? '✓' : ''}
28 </button>
29 );
30 })}
31 </section>
32 );
33}
341
2// src/components/OrderSummaryOptimized.tsx
3import React, { useMemo } from 'react';
4import { useSizeBase } from '../context/PizzaSizeBaseContext';
5import { useToppings } from '../context/PizzaToppingsContext';
6
7function calculatePrice(
8 size: 'small' | 'medium' | 'large' | null,
9 base: 'thin' | 'regular' | 'deep-dish' | null,
10 toppings: string[]
11): number {
12 let price = 0;
13
14 if (size === 'small') price += 8;
15 if (size === 'medium') price += 10;
16 if (size === 'large') price += 12;
17
18 if (base === 'thin') price += 0;
19 if (base === 'regular') price += 1;
20 if (base === 'deep-dish') price += 2;
21
22 price += toppings.length * 1.5;
23
24 return price;
25}
26
27export function OrderSummaryOptimized() {
28 const { state } = useSizeBase();
29 const { toppings, resetToppings } = useToppings();
30
31 const { size, base } = state;
32
33 const price = useMemo(
34 () => calculatePrice(size, base, toppings),
35 [size, base, toppings]
36 );
37
38 const isComplete = Boolean(size && base);
39
40 const handleReset = () => {
41 // reset size/base
42 // You can either lift a reset handler into a combined provider
43 // or call separate reset hooks; here we just show the idea.
44 window.location.reload(); // placeholder, see discussion below
45 };
46
47 return (
48 <aside>
49 <h2>Order summary</h2>
50 <p>Size: {size ?? 'Not selected'}</p>
51 <p>Base: {base ?? 'Not selected'}</p>
52 <p>
53 Toppings:{' '}
54 {toppings.length
55 ? toppings.join(', ')
56 : 'No toppings selected'}
57 </p>
58 <p>Total: ${price.toFixed(2)}</p>
59 <button onClick={handleReset}>Reset</button>
60 <button disabled={!isComplete}>
61 {isComplete ? 'Checkout' : 'Choose size & base first'}
62 </button>
63 </aside>
64 );
65}
66When to Move to a Dedicated Store Context works well for:
-
A few shared values that many components need
-
Mostly read-heavy data with modest update frequency
-
Once your Pizza Builder turns into a full restaurant management system with:
-
A cart with many pizzas
-
User profiles
-
Inventory and admin panels
-
Analytics and cross-page state
The mental model is the same as what we’ve practiced here: think in slices and owners. Context is the first step; a store is a scaling strategy when your pizza shop grows into a full platform.
Happy coding!