typescriptintermediate

React useReducer for Complex State

Manage complex component state with useReducer pattern including typed actions and middleware.

typescript
'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.