Kodokon kodokon.com

useReducer para estado complejo

Estructura el estado complejo con un reducer puro que centraliza las transiciones y descarta las combinaciones imposibles.

9 min · 3 preguntas

Abrir esta lección en Kodokon

En cuanto un componente orquesta varios valores de estado que evolucionan juntos - el estado de una petición, unos datos, un error - multiplicar las llamadas a useState dispersa la lógica de transición por cada manejador de eventos. Ya conoces el síntoma: tres setters que hay que llamar en el orden correcto y, tarde o temprano, un estado inconsistente, como un estado success junto a un error que no es nulo. useReducer invierte la responsabilidad: el estado se convierte en un único objeto y cada cambio pasa por una acción interpretada por una función central, el reducer.

JAVASCRIPT
function requestReducer(state, action) {
  switch (action.type) {
    case "start":
      return { status: "loading", data: null, error: null };
    case "resolve":
      return {
        status: "success",
        data: action.data,
        error: null,
      };
    case "reject":
      return {
        status: "error",
        data: null,
        error: action.error,
      };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
Cada transición reconstruye un estado completo y consistente.

El reducer es una función pura: recibe el estado actual y una acción, y luego devuelve el siguiente estado, sin efectos secundarios. Los beneficios son inmediatos: las transiciones son explícitas y exhaustivas, los estados imposibles desaparecen de raíz (cada case reconstruye un objeto consistente) y la lógica se lee en un solo sitio en lugar de estar dispersa. Una señal de alarma útil: si te encuentras copiando y pegando la misma secuencia de llamadas a setState en dos sitios, probablemente te falte un reducer.

JSX
function UserProfile() {
  const [state, dispatch] = useReducer(requestReducer, {
    status: "idle",
    data: null,
    error: null,
  });

  async function load(id) {
    dispatch({ type: "start" });
    try {
      const res = await fetch("/api/users/" + id);
      const data = await res.json();
      dispatch({ type: "resolve", data });
    } catch (error) {
      dispatch({ type: "reject", error });
    }
  }

  return <button onClick={() => load(1)}>Load</button>;
}
El componente expresa la intención, el reducer decide el estado.

Elegir entre useState y useReducer no es una cuestión de gusto. Reserva useState para valores simples e independientes; cambia a useReducer cuando el siguiente estado dependa del anterior o cuando varios campos cambien juntos. Dos ventajas adicionales: el tercer argumento aporta inicialización perezosa (useReducer(reducer, arg, init) solo ejecuta init en el primer render), y React garantiza que dispatch conserva la misma identidad de un render al siguiente - puedes pasarlo a los hijos sin useCallback.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué señal debería orientarte hacia useReducer en lugar de varias llamadas a useState?
    • El componente supera las cien líneas
    • Varios valores de estado cambian juntos y el nuevo estado depende del anterior
    • El componente recibe más de tres props
    • El estado contiene un array
  2. Un reducer devuelve el objeto de estado que recibió, mutado en el sitio. ¿Qué hace React?
    • Compara la referencia antigua y la nueva, las encuentra iguales y omite el re-render
    • Detecta la mutación profunda y vuelve a renderizar el componente
    • Lanza un error porque el estado está congelado en desarrollo
  3. ¿Por qué puedes pasar dispatch a un hijo memoizado sin ninguna precaución especial?
    • Porque React.memo ignora las props de tipo función
    • Porque dispatch se recrea pero es estructuralmente igual en cada render
    • Porque React garantiza que dispatch conserva la misma identidad durante toda la vida del componente