DEV Community

Cover image for 🔹 `useReducer` – Manejar estados complejos (4/8)
Pedro Alvarado
Pedro Alvarado

Posted on

🔹 `useReducer` – Manejar estados complejos (4/8)

📋 Tabla de contenidos

  1. useState – El fundamento del estado
  2. useEffect – Efectos secundarios y ciclo de vida
  3. useContext – Compartir estado sin props
  4. useReducer – Lógica de Estado Compleja y predecible
  5. useRef – Referencias, Almacenamiento y Escape
  6. useMemo y useCallback – Optimizando el Rendimiento
  7. Custom Hooks – Creando Lógica Reutilizable
  8. Errores Comunes y Soluciones

¿Qué es y para qué sirve?

useReducer es una alternativa a useState para gestionar lógicas de estado más complejas. Si te encuentras con un useState que maneja un objeto grande con muchas funciones de actualización diferentes y que se están volviendo difíciles de manejar, useReducer es la solución.

Centraliza toda la lógica de actualización del estado en una única función llamada "reducer", haciendo que el flujo de datos sea más predecible y fácil de depurar. Es conceptualmente similar a Redux, pero integrado en React.

Úsalo cuando:

  • El próximo estado depende de una lógica compleja basada en el estado anterior.
  • El estado tiene una estructura compleja (objetos anidados, arrays de objetos).
  • La lógica de actualización del estado necesita ser probada de forma aislada.
  • Quieres optimizar el rendimiento de componentes que disparan actualizaciones profundas, ya que dispatch no cambia entre renders.

Sintaxis y flujo de trabajo

const [state, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode
  • reducer: Una función pura (state, action) => newState que toma el estado actual y una "acción", y devuelve el nuevo estado.
  • initialState: El estado inicial, similar al de useState.
  • state: El estado actual.
  • dispatch: Una función que "despacha" acciones a la función reducer. Es la única forma de desencadenar una actualización de estado. Una acción es típicamente un objeto con una propiedad type (ej. { type: 'INCREMENT' }).

El flujo es: Componente → dispatch(action)reducer(state, action) → Nuevo Estado → Re-renderizado.


Ejemplo práctico detallado: lista de tareas (Todo List)

Este es el ejemplo canónico para useReducer, ya que implica múltiples tipos de actualizaciones sobre un array de objetos.

1. Definir el Reducer y el Estado Inicial

// components/TodoList.js

// Estado inicial: un array de tareas
const initialState = {
  todos: [
    { id: 1, text: 'Aprender useReducer', completed: true },
    { id: 2, text: 'Practicar con un ejemplo', completed: false },
  ],
};

// Acciones que podemos despachar
export const ACTIONS = {
  ADD_TODO: 'add-todo',
  TOGGLE_TODO: 'toggle-todo',
  DELETE_TODO: 'delete-todo',
};

// El reducer contiene toda la lógica de actualización
function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }],
      };
    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id),
      };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Usar useReducer en el Componente

import React, { useReducer, useState } from 'react';
// (el código del reducer y el estado inicial va aquí arriba)

function TodoList() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [newTodoText, setNewTodoText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (newTodoText.trim() === '') return;
    dispatch({ type: ACTIONS.ADD_TODO, payload: { text: newTodoText } });
    setNewTodoText('');
  };

  return (
    <div>
      <h3>Lista de Tareas con useReducer</h3>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Añadir nueva tarea"
        />
        <button type="submit">Añadir</button>
      </form>
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
            <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })}>
              {todo.completed ? 'Undo' : 'Completar'}
            </button>
            <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })}>
              Eliminar
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Buenas practicas y patrones comunes

  1. Reducers Puros: Un reducer nunca debe modificar el estado original (state) o la acción (action). Siempre debe devolver un nuevo objeto de estado. Tampoco debe realizar efectos secundarios (como llamadas a API).
  2. Usar payload para los datos: Es una convención común pasar los datos necesarios para la actualización dentro de una propiedad payload en el objeto de la acción. Ej: { type: 'ADD', payload: { newItem } }.
  3. Exportar constantes de acción: Definir los type de las acciones como constantes (como en el ejemplo ACTIONS) ayuda a evitar errores de tipeo y facilita el mantenimiento.
  4. Separar el reducer: Para componentes muy complejos o para reutilizar la lógica, puedes definir el reducer y el estado inicial en un archivo separado e importarlos.

🚨 Errores comunes y cómo evitarlos

  • Error: Mutar el estado en el reducer.

    // MAL ❌
    case ACTIONS.DELETE_TODO:
      // Esto modifica el array original, es un anti-patrón.
      const index = state.todos.findIndex(t => t.id === action.payload.id);
      state.todos.splice(index, 1);
      return state;
    
    • Solución: Siempre retorna una copia nueva del estado.
    // BIEN ✅
    case ACTIONS.DELETE_TODO:
      return { ...state, todos: state.todos.filter(t => t.id !== action.payload.id) };
    
  • Error: Olvidar el default case en el switch.

    • Solución: Si tu reducer recibe una acción que no reconoce, debería devolver el estado actual sin modificarlo. El default: return state; se encarga de esto. Sin él, tu reducer devolvería undefined y tu aplicación se rompería.

🚀 Retos prácticos

  1. Formulario Complejo: Convierte un formulario con varios campos (useState con un objeto) a un useReducer. Crea acciones para UPDATE_FIELD y RESET_FORM.
  2. Carrito de Compras: Gestiona un carrito de compras con acciones como ADD_ITEM, REMOVE_ITEM, INCREMENT_QUANTITY, DECREMENT_QUANTITY y CLEAR_CART.
  3. Juego Simple: Crea un juego simple (como un "clicker" con mejoras que puedes comprar) donde el estado del juego (puntos, mejoras, etc.) se gestione con useReducer.

Top comments (0)