Context API for shared state and patterns
Imagine this nightmare: You've built a slick order management dashboard. The Header needs order count, Sidebar shows customer details, OrderList displays status, and Checkout calculates totals. Suddenly, every change means passing props through 8+ components. Copy-paste madness begins. Sound familiar?
Context API is your escape hatch. It shares state across your entire app without the prop-drilling hell, perfect for order management systems where Header, Inventory, Cart, and Fulfillment all need the same data instantly.
Why Context API Matters
Traditional prop passing works for simple apps but breaks down in complex order management UIs where customer data, order status, and user preferences need to flow through multiple nested components. Context API provides a cleaner way to share this state globally or across subtrees.
From our earlier discussions:
- Props vs State in React — Real-World Use Cases
- React's Chain of Command: Parent-Child Communication
- Local State vs Global State Decision Framework
When building order dashboards with forms (like our controlled inputs), headers, sidebars, and lists all needing the same customer info or theme settings, Context becomes essential .
Complete Order Management App Setup
Create these files in your React project (npx create-react-app order-context-app). Every snippet compiles and runs immediately.
1. Context Creation (contexts/OrderContext.js)
// contexts/OrderContext.js
import React, { createContext, useContext, useState, useReducer } from 'react';
// Sample order data
const initialOrders = [
{ id: 1, customer: 'John Doe', amount: 299.99, status: 'pending' },
{ id: 2, customer: 'Jane Smith', amount: 149.50, status: 'shipped' }
];
// Reducer for order operations (advanced state management)
const orderReducer = (state, action) => {
switch (action.type) {
case 'ADD_ORDER':
return [...state, { id: state.length + 1, ...action.payload }];
case 'UPDATE_STATUS':
return state.map(order =>
order.id === action.payload.id
? { ...order, status: action.payload.status }
: order
);
case 'DELETE_ORDER':
return state.filter(order => order.id !== action.payload.id);
default:
return state;
}
};
const OrderContext = createContext();
export const OrderProvider = ({ children }) => {
const [orders, dispatch] = useReducer(orderReducer, initialOrders);
const [selectedCustomer, setSelectedCustomer] = useState('');
return (
<OrderContext.Provider value={{ orders, dispatch, selectedCustomer, setSelectedCustomer }}>
{children}
</OrderContext.Provider>
);
};
export const useOrders = () => {
const context = useContext(OrderContext);
if (!context) throw new Error('useOrders must be used within OrderProvider');
return context;
};2. Theme Context (contexts/ThemeContext.js)
// contexts/ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);3. Enhanced Order Form (components/EnhancedOrderForm.js)
Builds on our controlled inputs article but now uses Context.
// components/EnhancedOrderForm.js
import React, { useState } from 'react';
import { useOrders, useTheme } from '../contexts/OrderContext';
import { useTheme as useGlobalTheme } from '../contexts/ThemeContext';
function EnhancedOrderForm() {
const { selectedCustomer, setSelectedCustomer, dispatch } = useOrders();
const { theme } = useGlobalTheme();
const [amount, setAmount] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!selectedCustomer) {
setError('Please select a customer');
return;
}
if (!amount || Number(amount) <= 0) {
setError('Enter valid amount');
return;
}
dispatch({ type: 'ADD_ORDER', payload: { customer: selectedCustomer, amount: Number(amount), status: 'pending' } });
setAmount('');
setError('');
};
return (
<form onSubmit={handleSubmit} style={{ padding: '20px', background: theme === 'dark' ? '#333' : '#f5f5f5' }}>
<h3>Create Order {theme === 'dark' ? '🌙' : '☀️'}</h3>
<div>
<label>Customer: </label>
<select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
style={{ marginLeft: '10px', padding: '5px' }}
>
<option value="">Select Customer</option>
<option value="John Doe">John Doe</option>
<option value="Jane Smith">Jane Smith</option>
<option value="Bob Johnson">Bob Johnson</option>
</select>
</div>
<div style={{ marginTop: '10px' }}>
<label>Amount: $</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
min="0"
step="0.01"
style={{ marginLeft: '10px', padding: '5px' }}
/>
</div>
{error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
<button type="submit" style={{ marginTop: '10px', padding: '10px' }}>Add Order</button>
</form>
);
}
export default EnhancedOrderForm;Core Pattern: Eliminating Prop Drilling
Problem: In our previous parent-child communication examples, OrderList needed customer data passed through multiple components.
Solution: Context shares data directly.
4. Order List with Context (components/OrderList.js)
// components/OrderList.js
import React from 'react';
import { useOrders, useTheme as useOrderTheme } from '../contexts/OrderContext';
import { useTheme } from '../contexts/ThemeContext';
function OrderList() {
const { orders, dispatch } = useOrders();
const { theme } = useTheme();
const getStatusColor = (status) => {
const colors = { pending: 'orange', shipped: 'green', delivered: 'blue' };
return colors[status] || 'gray';
};
return (
<div style={{ padding: '20px' }}>
<h3>Orders ({orders.length})</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{orders.map(order => (
<li key={order.id} style={{
padding: '10px',
margin: '10px 0',
background: theme === 'dark' ? '#444' : '#fff',
borderLeft: `4px solid ${getStatusColor(order.status)}`
}}>
<strong>{order.customer}</strong> — ${order.amount.toFixed(2)}
<span style={{ float: 'right' }}>Status: <span style={{ color: getStatusColor(order.status) }}>{order.status}</span></span>
<div style={{ clear: 'both', marginTop: '5px' }}>
<button
onClick={() => dispatch({ type: 'UPDATE_STATUS', payload: { id: order.id, status: 'shipped' } })}
style={{ marginRight: '10px' }}
>
Mark Shipped
</button>
<button
onClick={() => dispatch({ type: 'DELETE_ORDER', payload: { id: order.id } })}
style={{ color: 'red' }}
>
Delete
</button>
</div>
</li>
))}
</ul>
</div>
);
}
export default OrderList;Try it: Notice how OrderList reads orders and dispatch directly from Context—no props needed!
Advanced Pattern: Multiple Contexts + Compound Components
Combine multiple contexts for complex UIs. Builds on our custom hooks and useEffect dependencies.
5. Dashboard Header (components/DashboardHeader.js)
// components/DashboardHeader.js
import React from 'react';
import { useOrders } from '../contexts/OrderContext';
import { useTheme, useTheme as useGlobalTheme } from '../contexts/ThemeContext';
function DashboardHeader() {
const { orders, selectedCustomer } = useOrders();
const { theme, toggleTheme } = useGlobalTheme();
const totalValue = orders.reduce((sum, order) => sum + order.amount, 0);
return (
<header style={{
padding: '20px',
background: theme === 'dark' ? '#222' : '#007bff',
color: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h1>Order Dashboard</h1>
<p>Selected: {selectedCustomer || 'No customer'} | Total Orders: {orders.length}</p>
</div>
<div>
<span>Total Value: ${totalValue.toFixed(2)}</span>
<button onClick={toggleTheme} style={{ marginLeft: '20px', padding: '10px' }}>
{theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
</button>
</div>
</header>
);
}
export default DashboardHeader;The App.js Wrapper
6. Main App (App.js)
// App.js
import React from 'react';
import { OrderProvider } from './contexts/OrderContext';
import { ThemeProvider } from './contexts/ThemeContext';
import DashboardHeader from './components/DashboardHeader';
import EnhancedOrderForm from './components/EnhancedOrderForm';
import OrderList from './components/OrderList';
function App() {
return (
<ThemeProvider>
<OrderProvider>
<div className="App">
<DashboardHeader />
<div style={{ display: 'flex', maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ flex: 1, padding: '20px' }}>
<EnhancedOrderForm />
</div>
<div style={{ flex: 2, padding: '20px' }}>
<OrderList />
</div>
</div>
</div>
</OrderProvider>
</ThemeProvider>
);
}
export default App;Run it: npm start. Add orders, change status, toggle theme—everything syncs automatically!
Pattern Comparison: Props vs Context
| Scenario | Prop Drilling | Context API |
|---|---|---|
| 2-3 levels deep | Fine | Overkill |
| Header + Form + List | Tedious | Clean |
| Multiple shared values | Nightmare | Perfect |
| Performance impact | None | Minimal (useSelector pattern) |
Advanced: Context + useReducer + useEffect
Building on our useState pitfalls and useEffect cleanup, here's Context with side effects:
7. Analytics Context (contexts/AnalyticsContext.js)
// contexts/AnalyticsContext.js
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useOrders } from './OrderContext';
const AnalyticsContext = createContext();
export const AnalyticsProvider = ({ children }) => {
const ordersRef = useRef();
const { orders } = useOrders();
useEffect(() => {
ordersRef.current = orders;
// Simulate analytics logging
const total = orders.reduce((sum, order) => sum + order.amount, 0);
console.log(`Analytics: Total orders value updated to $${total.toFixed(2)}`);
return () => {
console.log('Cleanup: Analytics unsubscribed');
};
}, [orders.length]); // Only re-run when order count changes
return (
<AnalyticsContext.Provider value={{ analyticsData: ordersRef.current }}>
{children}
</AnalyticsContext.Provider>
);
};
export const useAnalytics = () => useContext(AnalyticsContext);8. Analytics Context (components/AnalyticsPanel.js)
// components/AnalyticsPanel.js
import React from 'react';
import { useAnalytics } from '../contexts/AnalyticsContext';
import { useOrders } from '../contexts/OrderContext';
import { useTheme } from '../contexts/ThemeContext';
function AnalyticsPanel() {
const { analyticsData } = useAnalytics(); // ← CONSUMES ANALYTICS CONTEXT
const { orders } = useOrders();
const { theme } = useTheme();
const pendingCount = orders.filter(o => o.status === 'pending').length;
const avgOrderValue = orders.length > 0
? (orders.reduce((sum, o) => sum + o.amount, 0) / orders.length).toFixed(2)
: 0;
return (
<div style={{
background: theme === 'dark' ? '#444' : '#f9f9f9',
padding: '20px',
height: 'fit-content'
}}>
<h3>📊 Analytics</h3>
<div style={{ display: 'grid', gap: '15px', fontSize: '14px' }}>
<div><strong>Total Orders:</strong> {orders.length}</div>
<div><strong>Pending:</strong> {pendingCount}</div>
<div><strong>Avg Order Value:</strong> ${avgOrderValue}</div>
<div><strong>Last Snapshot:</strong> {analyticsData?.length || 0} orders</div>
</div>
<details style={{ marginTop: '20px' }}>
<summary>Debug: Check Console</summary>
<p>Open browser console to see analytics logging on order changes!</p>
</details>
</div>
);
}
export default AnalyticsPanel;7. Updated App.js
// App.js - COMPLETE WORKING VERSION WITH ANALYTICS
import React from 'react';
import { OrderProvider } from './contexts/OrderContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { AnalyticsProvider } from './contexts/AnalyticsContext'; // ← NOW IMPORTED
import DashboardHeader from './components/DashboardHeader';
import EnhancedOrderForm from './components/EnhancedOrderForm';
import OrderList from './components/OrderList';
import AnalyticsPanel from './components/AnalyticsPanel'; // ← NEW COMPONENT
function App() {
return (
<ThemeProvider>
<OrderProvider>
<AnalyticsProvider> {/* ← NOW WRAPPED */}
<div className="App">
<DashboardHeader />
<div style={{ display: 'flex', maxWidth: '1400px', margin: '0 auto' }}>
<div style={{ flex: 1, padding: '20px' }}>
<EnhancedOrderForm />
</div>
<div style={{ flex: 2, padding: '20px' }}>
<OrderList />
</div>
<div style={{ flex: 1, padding: '20px', borderLeft: '1px solid #ccc' }}>
<AnalyticsPanel /> {/* ← NOW USED */}
</div>
</div>
</div>
</AnalyticsProvider>
</OrderProvider>
</ThemeProvider>
);
}
export default App;When NOT to Use Context
- Simple parent-child (use props): Parent-Child Communication article
- Single component state: useState pitfalls
- Frequent updates (use
useMemo+ Context selectors)
Real-World Decision Framework
From our Local vs Global State article:
- Local state: Form inputs, single component UI
- Context: Shared across 3+ components (theme, selected customer)
- Redux: Complex async operations, 1000+ state items
Next article: Redux Toolkit best practices—when Context isn't enough for enterprise-scale order systems.
Member discussion