¿Qué es useReducer?
useReducer es un Hook de React que te permite manejar estados complejos en tus componentes de una manera más predecible y organizada. Es una alternativa a useState que resulta especialmente útil cuando tu estado tiene múltiples valores relacionados o cuando las actualizaciones dependen de valores anteriores.
¿Cuándo usar useReducer?
Deberías considerar useReducer cuando:
- Tu estado tiene múltiples valores que cambian juntos
- La lógica de actualización es compleja
- Quieres centralizar la lógica de estado en un solo lugar
- Necesitas optimizar el rendimiento pasando dispatch a componentes hijos
Conceptos básicos
useReducer se basa en tres conceptos fundamentales:
Estado (State): El valor actual de tus datos.
Acción (Action): Un objeto que describe qué cambio quieres hacer. Por convención, tiene una propiedad type que indica el tipo de acción.
Reducer: Una función pura que toma el estado actual y una acción, y devuelve el nuevo estado.
Sintaxis básica
const [state, dispatch] = useReducer(reducer, initialState);
El Hook retorna dos valores: el estado actual y una función dispatch para enviar acciones.
Ejemplo práctico
Vamos a crear un contador con múltiples operaciones para entender cómo funciona:
import { useReducer } from 'react';
// 1. Definir el reducer
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
case 'set':
return { count: action.payload };
default:
throw new Error(`Acción desconocida: ${action.type}`);
}
}
// 2. Usar el reducer en un componente
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Contador: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
+1
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-1
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
<button onClick={() => dispatch({ type: 'set', payload: 10 })}>
Establecer en 10
</button>
</div>
);
}
Ejemplo más complejo: Lista de tareas
Para ver el verdadero poder de useReducer, creemos un gestor de tareas:
function todoReducer(state, action) {
switch (action.type) {
case 'add':
return [
...state,
{ id: Date.now(), text: action.payload, completed: false }
];
case 'toggle':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'delete':
return state.filter(todo => todo.id !== action.payload);
case 'edit':
return state.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo
);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [input, setInput] = useState('');
const handleAdd = () => {
if (input.trim()) {
dispatch({ type: 'add', payload: input });
setInput('');
}
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Nueva tarea"
/>
<button onClick={handleAdd}>Agregar</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'toggle', payload: todo.id })}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'delete', payload: todo.id })}>
Eliminar
</button>
</li>
))}
</ul>
</div>
);
}
Inicialización diferida
Cuando el estado inicial requiere cálculos costosos, puedes usar una función inicializadora:
function init(initialCount) {
return { count: initialCount };
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
// ...
}
useReducer vs useState
Usa useState cuando:
- El estado es simple (un valor primitivo)
- No hay lógica compleja de actualización
- Los valores del estado son independientes
Usa useReducer cuando:
- El estado tiene múltiples valores relacionados
- Las actualizaciones son complejas
- Quieres testear la lógica de estado separadamente
- Necesitas optimizar renders de componentes hijos
Buenas prácticas
Mantén los reducers puros: no modifiques el estado directamente, siempre retorna un nuevo objeto. Define tipos de acciones como constantes para evitar errores tipográficos. Usa TypeScript para tipar acciones y estado, mejorando la seguridad. Considera combinar useReducer con useContext para gestión de estado global. Maneja casos default en tus reducers para detectar errores.
Conclusión
useReducer es una herramienta poderosa para manejar estado complejo en React. Aunque tiene una curva de aprendizaje inicial, te ayudará a escribir código más mantenible y predecible cuando tu aplicación crece en complejidad.