Saltar al contenido

Lección 1 de 15 · Lectura y práctica

useReducer: Estado complejo simplificado

CONCEPTO CLAVE: useReducer es un hook de React que te permite manejar estado complejo mediante una función reductora (reducer). A diferencia de useState, que maneja cambios simples, useReducer es ideal cuando tienes múltiples.

Del concepto a la práctica

Taller de esta lección

Explicación

Un reducer resulta útil cuando una reserva cambia varias partes relacionadas: lista, disponibilidad y mensaje. La función debe ser pura: recibe un estado y una acción, y devuelve otro estado sin modificar el anterior.

Ejemplo resuelto

js
const inicial = { equipos: [{ id: 1, disponible: true }], aviso: "" };
function reducer(state, action) {
  if (action.type === "reservar") return { equipos: state.equipos.map(e => e.id === action.id ? { ...e, disponible: false } : e), aviso: "Reserva creada" };
  return state;
}
console.log(reducer(inicial, { type: "reservar", id: 1 }).equipos[0].disponible); // false

Pruébalo

  1. Añade una acción liberar.

  2. Conserva inmutables los equipos no afectados.

  3. Comprueba ambos estados con console.log.

Error frecuente: cambiar state.equipos[0].disponible; corrígelo creando un nuevo objeto con propagación.

Comprueba tu resultado

Ejercicio: usa dos equipos, reserva solo el segundo y verifica que el primero conserva true, el segundo queda en false y el aviso coincide con la acción.

Prueba además que inicial.equipos[0].disponible sigue siendo true tras reservar y que una acción desconocida devuelve el estado anterior. Define qué ocurre con un id inexistente: en este taller conserva estado y aviso, en lugar de mostrar una reserva creada sin equipo.

Estado con tarea t1 pendiente y acción TOGGLE t1 entran en un reducer puro; sale un nuevo estado con done true, sin mutación ni efectos secundarios.
CursaloMapa de práctica
El reducer decide la transición

El reducer recibe estado y acción y devuelve el próximo estado sin modificar el anterior. TOGGLE de t1 cambia pendiente a completa; la red y otras consecuencias quedan fuera de esa función pura.

Ver la infografía en tamaño completo ↗
CONCEPTO CLAVE: useReducer es un hook de React que te permite manejar estado complejo mediante una función reductora (reducer). A diferencia de useState, que maneja cambios simples, useReducer es ideal cuando tienes múltiples sub-estados o acciones que dependen unas de otras, o cuando la lógica para actualizar el estado es compleja y predecible.

¿Qué problema resuelve useReducer?

Imaginemos que tenemos un componente de carrito de compras. Con useState, tendríamos algo así:

const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [discount, setDiscount] = useState(0);
const [shipping, setShipping] = useState(0);

// Función para agregar producto
const addItem = (product) => {
  const newItems = [...items, product];
  setItems(newItems);
  setTotal(calculateTotal(newItems));
  setShipping(calculateShipping(newItems));
  setDiscount(calculateDiscount(newItems));
};

Este código tiene varios problemas:

  • Múltiples estados que dependen entre sí
  • Lógica dispersa en diferentes funciones
  • Difícil de mantener y predecir
  • Posibles inconsistencias si un setState falla

Con useReducer, centralizamos toda esta lógica:

const [state, dispatch] = useReducer(cartReducer, initialState);

const addItem = (product) => {
  dispatch({ type: 'ADD_ITEM', payload: product });
};
💡 Regla mnemotécnica: Piensa en useReducer como una máquina expendedora. Le envías una acción (moneda + selección) y el reducer (máquina) decide qué hacer con ella y te devuelve un nuevo estado (el producto).

Sintaxis de useReducer

La sintaxis básica es:

const [state, dispatch] = useReducer(reducer, initialState, init?);
ParámetroDescripción
reducerFunción que recibe el estado actual y una acción, y devuelve el nuevo estado
initialStateEl estado inicial de tu componente
init (opcional)Función para calcular el estado inicial lazymente

Estructura del Reducer

El reducer es una función pura con esta estructura:

function reducer(state, action) {
  switch (action.type) {
    case 'ACCION_1':
      return { ...state, /* cambios */ };
    case 'ACCION_2':
      return { ...state, /* cambios */ };
    default:
      return state;
  }
}
📌 Recuerda: El reducer SIEMPRE debe ser una función pura. Esto significa que dados los mismos argumentos, siempre debe devolver el mismo resultado, sin efectos secundarios ni mutaciones del estado anterior.

Ejemplo Práctico: Formulario de Tareas

Vamos a construir un gestor de tareas completo con useReducer:

import { useReducer } from 'react';

// Estado inicial
const initialState = {
  tasks: [],
  filter: 'all', // 'all', 'active', 'completed'
  editingId: null,
};

// El reducer
function taskReducer(state, action) {
  switch (action.type) {
    case 'ADD_TASK':
      return {
        ...state,
        tasks: [
          ...state.tasks,
          {
            id: Date.now(),
            text: action.payload,
            completed: false,
            createdAt: new Date().toISOString(),
          },
        ],
      };

    case 'TOGGLE_TASK':
      return {
        ...state,
        tasks: state.tasks.map((task) =>
          task.id === action.payload
            ? { ...task, completed: !task.completed }
            : task
        ),
      };

    case 'DELETE_TASK':
      return {
        ...state,
        tasks: state.tasks.filter((task) => task.id !== action.payload),
      };

    case 'EDIT_TASK':
      return {
        ...state,
        tasks: state.tasks.map((task) =>
          task.id === action.payload.id
            ? { ...task, text: action.payload.text }
            : task
        ),
        editingId: null,
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload,
      };

    case 'START_EDITING':
      return {
        ...state,
        editingId: action.payload,
      };

    case 'CLEAR_COMPLETED':
      return {
        ...state,
        tasks: state.tasks.filter((task) => !task.completed),
      };

    default:
      return state;
  }
}

// Componente
function TaskManager() {
  const [state, dispatch] = useReducer(taskReducer, initialState);
  const [newTaskText, setNewTaskText] = useState('');

  // Derivar datos filtrados del estado
  const filteredTasks = state.tasks.filter((task) => {
    if (state.filter === 'active') return !task.completed;
    if (state.filter === 'completed') return task.completed;
    return true;
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    if (newTaskText.trim()) {
      dispatch({ type: 'ADD_TASK', payload: newTaskText.trim() });
      setNewTaskText('');
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={newTaskText}
          onChange={(e) => setNewTaskText(e.target.value)}
          placeholder="Nueva tarea..."
        />
        <button type="submit">Agregar</button>
      </form>

      <div className="filters">
        {['all', 'active', 'completed'].map((filter) => (
          <button
            key={filter}
            onClick={() => dispatch({ type: 'SET_FILTER', payload: filter })}
            className={state.filter === filter ? 'active' : ''}
          >
            {filter}
          </button>
        ))}
      </div>

      <ul>
        {filteredTasks.map((task) => (
          <li key={task.id} className={task.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TASK', payload: task.id })}
            />
            {task.text}
            <button onClick={() => dispatch({ type: 'DELETE_TASK', payload: task.id })}>
              Eliminar
            </button>
          </li>
        ))}
      </ul>

      <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
        Limpiar completadas
      </button>
    </div>
  );
}
⚠️ Importante: Nunca mutes el estado directamente. Siempre usa el spread operator o métodos que creen nuevos arrays/objetos. En el ejemplo, state.tasks.map() y state.tasks.filter() crean nuevos arrays.

Comparación: useState vs useReducer

CriteriouseStateuseReducer
Complejidad del estadoIdeal para estados simples (1-2 valores)Ideal para estados complejos con múltiples sub-valores
AccionesMúltiples setters dispersosAcciones centralizadas y predecibles
DebuggingDifícil rastrear cambiosFácil con Redux DevTools
RendimientoActualizaciones individualesOptimizado para actualizaciones batch
TestabilidadRequiere renderizar componenteReducer testeable aisladamente
📌 Regla práctica: Usa useState cuando tengas un valor booleano simple o un número. Usa useReducer cuando tengas 3 o más valores relacionados, o cuando la lógica de actualización sea compleja.

Patrones Avanzados con useReducer

1. Uso de Action Creators

Para reutilizar lógica de dispatch y evitar strings mágicos:

// Action creators
const actions = {
  addTask: (text) => ({ type: 'ADD_TASK', payload: text }),
  toggleTask: (id) => ({ type: 'TOGGLE_TASK', payload: id }),
  deleteTask: (id) => ({ type: 'DELETE_TASK', payload: id }),
  setFilter: (filter) => ({ type: 'SET_FILTER', payload: filter }),
};

// Uso
dispatch(actions.addTask('Nueva tarea'));
dispatch(actions.toggleTask(taskId));

2. Inicialización Lazy

Calcula el estado inicial de forma diferida:

function init(savedState) {
  return {
    tasks: savedState?.tasks || [],
    filter: savedState?.filter || 'all',
    lastUpdated: new Date().toISOString(),
  };
}

const [state, dispatch] = useReducer(reducer, savedState, init);

// Esto es equivalente a:
// useReducer(reducer, init(savedState))
💡 Beneficio: La inicialización lazy es útil cuando el cálculo inicial es costoso o cuando necesitas procesar datos antes de establecer el estado (como leer del localStorage).

3. Reducers con Thunks

Para acciones asíncronas, puedes combinar useReducer con useEffect:

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

function DataComponent() {
  const [state, dispatch] = useReducer(reducer, {
    data: null,
    loading: false,
    error: null,
  });

  const fetchData = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const data = await api.getData();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };

  return (
    <div>
      {state.loading && <p>Cargando...</p>}
      {state.error && <p>Error: {state.error}</p>}
      {state.data && <p>Datos: {JSON.stringify(state.data)}</p>}
      <button onClick={fetchData}>Obtener datos</button>
    </div>
  );
}

Combinando useReducer con Context

Para estado global compartido:

const TaskContext = createContext();

function TaskProvider({ children }) {
  const [state, dispatch] = useReducer(taskReducer, initialState);

  return (
    <TaskContext.Provider value={{ state, dispatch }}>
      {children}
    </TaskContext.Provider>
  );
}

// En cualquier componente hijo
function TaskList() {
  const { state, dispatch } = useContext(TaskContext);
  // ... usa state y dispatch
}
📌 Patrón recomendado: Esta combinación es similar a Redux pero más simple y sin dependencias externas. Perfecto para aplicaciones de tamaño mediano.
Ver más: Ejemplo completo con Context y localStorage
import { createContext, useContext, useReducer, useEffect } from 'react';

const TaskContext = createContext();

const STORAGE_KEY = 'tasks_state';

function loadInitialState() {
  try {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
      const parsed = JSON.parse(saved);
      return { ...initialState, ...parsed };
    }
  } catch (e) {
    console.error('Error loading from localStorage:', e);
  }
  return initialState;
}

function taskReducer(state, action) {
  // ... reducer logic
}

export function TaskProvider({ children }) {
  const [state, dispatch] = useReducer(
    taskReducer,
    undefined,
    () => loadInitialState()
  );

  // Persistir en localStorage
  useEffect(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  }, [state]);

  return (
    <TaskContext.Provider value={{ state, dispatch }}>
      {children}
    </TaskContext.Provider>
  );
}

export function useTasks() {
  const context = useContext(TaskContext);
  if (!context) {
    throw new Error('useTasks debe usarse dentro de TaskProvider');
  }
  return context;
}

Errores Comunes y Cómo Evitarlos

  1. Mutación directa del estado: Usa siempre spread operator o métodos inmutables. state.items.push() romperá React.
  2. Olvidar el default en el switch: Siempre incluye default: return state; para acciones desconocidas.
  3. Referencias circulares en initialState: Evita que el estado inicial se referencie a sí mismo.
  4. No separar el reducer: Mantén el reducer como función separada para mejor testabilidad.
⚠️ Debugging tip: Usa console.log o Redux DevTools para rastrear las acciones. En desarrollo, puedes crear un wrapper de dispatch que registre todas las acciones:
function useReducerWithLogger(reducer, initialState) {
  const [state, dispatch] = useReducer(reducer, initialState);

  const dispatchWithLogger = (action) => {
    console.log('Estado actual:', state);
    console.log('Acción:', action);
    dispatch(action);
  };

  return [state, dispatchWithLogger];
}
El reducer bien diseñado es como código bien escrito: expresivo, predecible y fácil de modificar. Invierte tiempo en diseñarlo y te ahorrará mucho más tiempo después.

Cuándo NO usar useReducer

Aunque useReducer es poderoso, no siempre es la mejor opción. Evita usarlo cuando:

  • Tu estado es simple (un boolean, un string, un número)
  • Las actualizaciones son independientes entre sí
  • El componente es pequeño y no tiene lógica compleja
  • Estás prototipando rápidamente
💡 Consejo: Si sientes que estás escribiendo mucho código para una tarea simple, probablemente useState es mejor opción. Usa la herramienta correcta para cada trabajo.

Resumen de la Lección

En esta lección hemos aprendido:

  • Qué es useReducer y cómo difiere de useState para manejo de estado complejo
  • La estructura del reducer: función pura que recibe estado y acción, devuelve nuevo estado
  • Actions con type y payload: convención estándar para describir qué cambió y con qué datos
  • Patrones avanzados: action creators, inicialización lazy, combinación con Context
  • Cuándo usar useReducer: estado complejo, múltiples sub-estados relacionados, lógica predecible
🧠 Quiz

¿Cuál es la principal ventaja de usar useReducer sobre múltiples llamadas a useState?

  • A) Es más rápido en ejecución
  • B) Centraliza la lógica de actualización y hace el código más predecible
  • C) Usa menos memoria
  • D) Funciona sin React
✅ Respuesta correcta: B. useReducer centraliza toda la lógica de actualización en un solo lugar (el reducer), haciendo el código más mantenible, testeable y predecible. Aunque useState puede ser ligeramente más rápido en casos simples, la ventaja principal de useReducer es la organización y previsibilidad del código.
🧠 Quiz

¿Cuál de las siguientes afirmaciones sobre el reducer es CORRECTA?

  • A) Puede modificar el estado directamente usando state.nuevaPropiedad = valor
  • B) Debe ser una función pura que no mute el estado original
  • C) Solo puede manejar un tipo de acción
  • D) Debe usar async/await para operaciones asíncronas
✅ Respuesta correcta: B. El reducer SIEMPRE debe ser una función pura que no muta el estado. Debe devolver un nuevo objeto/array de estado, nunca modificar el anterior. Esto es fundamental para que React pueda detectar cambios y re-renderizar correctamente.

Próximos Pasos

Ahora que dominas useReducer, estás listo para:

  • Explorar useContext + useReducer para estado global sin Redux
  • Aprender Custom Hooks que abstraen lógica con useReducer
  • Profundizar en patrones de estado como reducer composition
📌 Preparación: En la próxima lección exploraremos cómo crear Custom Hooks que encapsulen lógica con useReducer, permitiéndote reutilizar patrones de estado complejos en toda tu aplicación.

Práctica acompañada: Una interfaz web de principio a fin

Estado, formularios y una API pequeña. Usa este cuaderno como práctica complementaria de la ruta; el material original de la lección continúa siendo tu referencia.

Estado, formularios y una API pequeña: HTML, Estado, Validación, Respuesta
De la idea al resultado: una secuencia para practicar y revisar.

Práctica: cuatro pruebas de comportamiento

Prueba un título normal, solo espacios, 121 caracteres y una falla de red simulada. Registra la respuesta y el estado de la interfaz. Repite usando solo teclado: debes llegar al campo, enviarlo y encontrar el error sin usar el ratón.

Después agrega una segunda tarea y cambia el orden de la lista. Comprueba que cada botón sigue actuando sobre el elemento correcto. Si trabajas con microservicios, dibuja qué servicio posee el dato y qué ocurre si la dependencia no responde. No necesitas una infraestructura distribuida para aprender a definir tiempos de espera y errores comprensibles.

Comprueba tu entrega

Entrega el código, el contrato de API y evidencia de las cuatro pruebas. Indica si los datos se guardan o desaparecen al recargar. Una demostración en memoria debe decirlo para que nadie confunda un prototipo con almacenamiento persistente.

Sigue con React Intermedio: Dominando Componentes y Patrones Avanzados

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.