typescriptintermediate
React useReducer for Complex State
Manage complex component state with useReducer pattern including typed actions and middleware.
typescriptPress ā/Ctrl + Shift + C to copy
'use client';
import { useReducer, Dispatch } from 'react';
// Define state
interface CartState {
items: { id: string; name: string; price: number; qty: number }[];
discount: number;
loading: boolean;
}
// Define actions
type CartAction =
| { type: 'ADD_ITEM'; payload: { id: string; name: string; price: number } }
| { type: 'REMOVE_ITEM'; payload: { id: string } }
| { type: 'UPDATE_QTY'; payload: { id: string; qty: number } }
| { type: 'APPLY_DISCOUNT'; payload: { percent: number } }
| { type: 'CLEAR_CART' }
| { type: 'SET_LOADING'; payload: boolean };
const initialState: CartState = { items: [], discount: 0, loading: false };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find((i) => i.id === action.payload.id);
if (existing) {
return {
...state,
items: state.items.map((i) =>
i.id === action.payload.id ? { ...i, qty: i.qty + 1 } : i
),
};
}
return { ...state, items: [...state.items, { ...action.payload, qty: 1 }] };
}
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter((i) => i.id !== action.payload.id) };
case 'UPDATE_QTY':
return {
...state,
items: state.items.map((i) =>
i.id === action.payload.id ? { ...i, qty: Math.max(0, action.payload.qty) } : i
).filter((i) => i.qty > 0),
};
case 'APPLY_DISCOUNT':
return { ...state, discount: action.payload.percent };
case 'CLEAR_CART':
return initialState;
case 'SET_LOADING':
return { ...state, loading: action.payload };
default:
return state;
}
}
// Selectors
const getSubtotal = (state: CartState) => state.items.reduce((sum, i) => sum + i.price * i.qty, 0);
const getTotal = (state: CartState) => getSubtotal(state) * (1 - state.discount / 100);
const getItemCount = (state: CartState) => state.items.reduce((sum, i) => sum + i.qty, 0);
// Component
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
<div>
<p>Items: {getItemCount(state)} | Total: ${getTotal(state).toFixed(2)}</p>
{state.items.map((item) => (
<div key={item.id} className="flex justify-between p-2">
<span>{item.name} x{item.qty}</span>
<div>
<button onClick={() => dispatch({ type: 'UPDATE_QTY', payload: { id: item.id, qty: item.qty - 1 } })}>-</button>
<button onClick={() => dispatch({ type: 'UPDATE_QTY', payload: { id: item.id, qty: item.qty + 1 } })}>+</button>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>Ć</button>
</div>
</div>
))}
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>Clear</button>
</div>
);
}Use Cases
- Shopping cart state management
- Complex form state with multiple fields
- State machines with typed transitions
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptadvanced
React Compound Component Pattern
Build flexible compound components using React context for shared state between related parts.
Best for: Building flexible component libraries
#react#patterns
typescriptadvanced
Context Selector Pattern
Avoid unnecessary re-renders with a context selector pattern that subscribes to specific state slices.
Best for: High-performance global state
#react#context
typescriptadvanced
Suspense Data Fetching Pattern
Use React Suspense for data fetching with resource caching, error boundaries, and streaming SSR.
Best for: Suspense-first data loading
#react#suspense
typescriptintermediate
Error Boundary with Fallback UI
Class-based error boundary component that catches render errors and displays a customizable fallback UI.
Best for: Graceful error recovery
#error-boundary#error-handling