Patrón reducer + context para estado complejo

Lectura
30 min~5 min lectura

Concepto clave

El patrón reducer + context combina el hook useReducer con Context API para manejar estado complejo de forma predecible y escalable. Piensa en un reducer como un semáforo: recibe el estado actual y una acción (como 'cambiar a rojo') y devuelve el nuevo estado. El context actúa como un tablero central que cualquier componente puede consultar sin pasar props manualmente. Este patrón es ideal cuando tienes múltiples valores de estado que cambian juntos (como un carrito de compras o un formulario multi-paso) y necesitas lógica de actualización centralizada.

La clave está en separar la lógica de estado de los componentes. El reducer define cómo cambia el estado en respuesta a acciones, mientras que el context provee el estado y el dispatch a toda la aplicación. Esto evita el 'prop drilling' y hace que los cambios sean trazables, similar a cómo Redux maneja el estado, pero sin librerías externas.

Cómo funciona en la práctica

Imagina que construyes un carrito de compras. El estado incluye: items, cantidad total y precio total. Las acciones son: agregar item, eliminar item, limpiar carrito. En lugar de usar múltiples useState, defines un reducer que maneja estas acciones. Luego, creas un contexto que expone el estado y el dispatch. Los componentes hijos simplemente llaman a dispatch con la acción correspondiente.

Paso a paso: 1) Define el estado inicial y el reducer. 2) Crea el contexto con createContext. 3) En un proveedor, usa useReducer y pasa el estado y dispatch al context. 4) Envuelve tu app con el proveedor. 5) En cualquier componente, usa useContext para acceder al estado o dispatch.

Código en acción

Ejemplo funcional de carrito de compras:

import React, { createContext, useContext, useReducer } from 'react';

const CartContext = createContext();

const initialState = { items: [], totalQuantity: 0, totalPrice: 0 };

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const newItems = [...state.items, action.payload];
      return {
        items: newItems,
        totalQuantity: state.totalQuantity + 1,
        totalPrice: state.totalPrice + action.payload.price
      };
    }
    case 'REMOVE_ITEM': {
      const newItems = state.items.filter(item => item.id !== action.payload.id);
      const removedItem = state.items.find(item => item.id === action.payload.id);
      return {
        items: newItems,
        totalQuantity: state.totalQuantity - 1,
        totalPrice: state.totalPrice - (removedItem ? removedItem.price : 0)
      };
    }
    case 'CLEAR_CART':
      return initialState;
    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);
  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error('useCart must be used within a CartProvider');
  return context;
}

Uso en un componente:

import { useCart } from './CartContext';

function Product({ product }) {
  const { dispatch } = useCart();
  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
        Agregar al carrito
      </button>
    </div>
  );
}

function CartSummary() {
  const { state } = useCart();
  return (
    <div>
      <p>Items: {state.totalQuantity}</p>
      <p>Total: ${state.totalPrice.toFixed(2)}</p>
    </div>
  );
}

Errores comunes

  1. Mutación directa del estado: En el reducer, nunca hagas state.items.push(...). Siempre crea un nuevo array o copia. Ejemplo: newItems = [...state.items, action.payload].
  2. Olvidar el caso default: Siempre retorna el estado actual en default para evitar errores si se despacha una acción desconocida.
  3. Contexto no envuelto: Si usas useCart fuera del CartProvider, obtendrás undefined. Siempre verifica que el contexto exista y lanza un error claro.
  4. Pasar objetos complejos en dispatch: Las acciones deben ser planas y serializables. Evita funciones o referencias circulares.
  5. No memoizar el valor del provider: Si pasas un objeto nuevo cada render, todos los consumidores se re-renderizan. Usa useMemo para estabilizar el valor.

Checklist de dominio

  • Define un reducer con estado inicial, acciones y casos para cada tipo de acción.
  • Crea un contexto con createContext y un proveedor que use useReducer.
  • Exporta un hook personalizado (useCart) que devuelva el contexto y valide su existencia.
  • Usa dispatch en componentes para modificar el estado, sin mutar directamente.
  • Implementa al menos 3 tipos de acciones (agregar, eliminar, limpiar).
  • Evita re-renderizados innecesarios separando contextos si es necesario (ej. estado y dispatch por separado).
  • Prueba el flujo completo: agregar items, ver total, eliminar y limpiar.

Crear un gestor de tareas con reducer + context

Objetivo

Construye una aplicación de lista de tareas (TODO) usando el patrón reducer + context. El estado debe incluir: arreglo de tareas (cada tarea con id, texto, completada) y un filtro (todas, activas, completadas). Las acciones: agregar tarea, toggle completada, eliminar tarea, cambiar filtro.

Pasos

  1. Define el estado inicial y el reducer en un archivo todoReducer.js.
  2. Crea el contexto y el proveedor en TodoContext.js.
  3. Exporta un hook useTodo que retorne el estado y dispatch.
  4. Crea un componente TodoApp que use el proveedor y renderice: un formulario para agregar tareas, una lista filtrada y botones de filtro.
  5. Asegúrate de que al agregar una tarea, se genere un id único (puedes usar Date.now()).
  6. Implementa el filtro: si el filtro es 'activas', solo muestra tareas no completadas; si 'completadas', solo las completadas; 'todas' muestra todas.
  7. Estiliza mínimamente (puedes usar CSS básico, no es necesario framework).

Entregable

Un archivo TodoApp.jsx que exporte el componente principal, y los archivos auxiliares todoReducer.js y TodoContext.js. La aplicación debe ser funcional y permitir agregar, marcar como completada, eliminar y filtrar tareas.

Mini-rúbrica de evaluación

  • Reducer maneja correctamente 4 acciones: ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER (25%).
  • Contexto provee estado y dispatch, y el hook useTodo valida el contexto (25%).
  • Componente TodoApp usa el contexto y muestra la lista filtrada correctamente (25%).
  • No hay mutaciones directas del estado; se usan operadores inmutables como spread o filter (15%).
  • El código es legible y está organizado en archivos separados (10%).
Pistas
  • Para el filtro, guarda un string 'all', 'active', 'completed' en el estado. En el selector, usa filter según ese valor.
  • Usa useReducer dentro del proveedor y pasa [state, dispatch] como valor del contexto. No olvides memoizar el valor si es necesario.
  • Para eliminar una tarea, usa filter con el id. Para toggle, mapea el arreglo y cambia la propiedad completed del elemento coincidente.