Pattern that suck (and what to do instead)

Let's talk about a pattern I see everywhere in React - and why I think it often does more harm than good.

We'll use shopping cart as an example. You know the drill, users can:

Seems simple enough, right?

Now here's the question:
Would you rather build this with one giant switch statement, or use a set of functions that each do one thing?

Let's walk through both.

The “One function to rule them all” pattern (aka reducer)

The general premise is to have an action and switch based on its type.

interface CartItem {
  id: string;
  quantity: number;
  name: string;
}

type CartAction =
  | { readonly type: "ADD_ITEM"; payload: CartItem }
  | { readonly type: "REMOVE_ITEM"; payload: { id: string } }
  | { readonly type: "INCREASE_QUANTITY"; payload: { id: string } }
  | { readonly type: "DECREASE_QUANTITY"; payload: { id: string } };

type CartState = CartItem[];

function reducer(items: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = items.find(item => item.id === action.payload.id);
      if (existing) {
        return items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
      }
      return [...items, { ...action.payload, quantity: 1 }];
    }

    case 'REMOVE_ITEM': {
      return items.filter(item => item.id !== action.payload.id);
    }

    case 'INCREASE_QUANTITY': {
      return items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
    }

    case 'DECREASE_QUANTITY': {
      return items
          .map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity - 1 }
              : item
          )
          .filter(item => item.quantity > 0)
    }
    // There are a few ways to handle an exhaustive switch, but all of them are kind of annoying
  }
  return items;
}

function useCart(initial: CartState = []) {
  const [state, dispatch] = useReducer(reducer, initial)
  
  return [state, dispatch]
}

Then you use it like this:

<ul>
  {items.map(item => (
    <li key={item.id}>
      {item.name} × {item.quantity} – ${(item.price * item.quantity).toFixed(2)}
      <button onClick={() => dispatch({ type: "INCREASE_QUANTITY", payload: { id: item.id } })}>+</button>
      <button onClick={() => dispatch({ type: "DECREASE_QUANTITY", payload: { id: item.id } })}>-</button>
      <button onClick={() => dispatch({ type: "REMOVE_ITEM", payload: { id: item.id } })}>Remove</button>
    </li>
  ))}
</ul>

Seems okay... but now imagine 10 more actions, 3 more conditions per case, and some async validation.

IMHO it's not pleasant to work with.

The other way: just use functions

Now let's see what happens if we stop pretending this is a Redux app from 2016 and just write normal code:

function useCart(initial: CartItem[] = []) {
  const [cart, setCart] = useState<CartItem[]>(initial);

  const addItem = (item: CartItem) => {
    setCart(prev => {
      const existing = prev.find(p => p.id === item.id);
      if (existing) {
        return prev.map(p =>
          p.id === item.id ? { ...p, quantity: p.quantity + 1 } : p
        );
      }
      return [...prev, { ...item, quantity: 1 }];
    });
  };

  const removeItem = (id: string) => {
    setCart(prev => prev.filter(p => p.id !== id));
  };

  const increaseQuantity = (id: string) => {
    setCart(prev =>
      prev.map(p =>
        p.id === id ? { ...p, quantity: p.quantity + 1 } : p
      )
    );
  };

  const decreaseQuantity = (id: string) => {
    setCart(prev =>
      prev
        .map(p =>
          p.id === id ? { ...p, quantity: p.quantity - 1 } : p
        )
        .filter(p => p.quantity > 0)
    );
  };

  return {
    cart,
    addItem,
    removeItem,
    increaseQuantity,
    decreaseQuantity,
  };
}

Now your usage looks like this:

<ul>
  {cart.map(item => (
    <li key={item.id}>
      {item.name} × {item.quantity} – ${(item.price * item.quantity).toFixed(2)}
      <button onClick={() => increaseQuantity(item.id)}>+</button>
      <button onClick={() => decreaseQuantity(item.id)}>-</button>
      <button onClick={() => removeItem(item.id)}>Remove</button>
    </li>
  ))}
</ul>

Why is this better?

So... Why Even Use Reducers?

People say it's better for bigger state, but honestly? The only real difference here is the switch statement versus calling smaller functions directly. Optimization? The React compiler will add useCallbacks and performance should be the same, unless I'm missing something? So you probably should never use reducers.

Agree? Disagree? Let me know.