¿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 });
};Sintaxis de useReducer
La sintaxis básica es:
const [state, dispatch] = useReducer(reducer, initialState, init?);| Parámetro | Descripción |
|---|---|
reducer | Función que recibe el estado actual y una acción, y devuelve el nuevo estado |
initialState | El 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;
}
}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>
);
}state.tasks.map() y state.tasks.filter() crean nuevos arrays.Comparación: useState vs useReducer
| Criterio | useState | useReducer |
|---|---|---|
| Complexidad del estado | Ideal para estados simples (1-2 valores) | Ideal para estados complejos con múltiples sub-valores |
| Acciones | Múltiples setters dispersos | Acciones centralizadas y predecibles |
| Debugging | Difícil rastrear cambios | Fácil con Redux DevTools |
| Rendimiento | Actualizaciones individuales | Optimizado para actualizaciones batch |
| Testabilidad | Requiere renderizar componente | Reducer testeable aisladamente |
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))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
}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
- Mutación directa del estado: Usa siempre spread operator o métodos inmutables.
state.items.push()romperá React. - Olvidar el default en el switch: Siempre incluye
default: return state;para acciones desconocidas. - Referencias circulares en initialState: Evita que el estado inicial se referencie a sí mismo.
- No separar el reducer: Mantén el reducer como función separada para mejor testabilidad.
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
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
¿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
¿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
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