Introducción
En el artículo anterior sobre useReducer, vimos cómo manejar estado complejo usando el patrón tradicional con switch. Hoy vamos a explorar una alternativa más elegante y moderna: el Reducer Object Pattern. Esta técnica te permitirá escribir reducers más limpios, legibles y fáciles de mantener.
¿Qué problema resuelve?
Cuando usamos switch en nuestros reducers, el código puede volverse verbose y repetitivo. Mira este ejemplo tradicional:
function reducer(state, action) {
switch (action.type) {
case 'ERROR':
return {
...state,
error: true,
loading: false,
};
case 'CHECK':
return {
...state,
loading: true,
};
case 'SUCCESS':
return {
...state,
error: false,
loading: false,
data: action.payload,
};
default:
return state;
}
}
Este código funciona perfectamente, pero tiene algunas desventajas:
- Mucho boilerplate con
case,breakoreturn - Difícil de leer cuando tienes muchas acciones
- Propenso a errores si olvidas el
returno elbreak
El Reducer Object Pattern
El patrón de Reducer Object consiste en usar un objeto que mapea los tipos de acción a sus transformaciones de estado correspondientes. Aquí está la magia:
const reducerObject = (state) => ({
'ERROR': {
...state,
error: true,
loading: false,
},
'CHECK': {
...state,
loading: true,
},
'SUCCESS': {
...state,
error: false,
loading: false,
data: state.tempData,
}
});
const reducer = (state, action) => {
if (reducerObject(state)[action.type]) {
return reducerObject(state)[action.type];
} else {
return state;
}
};
¿Cómo funciona?
- reducerObject: Es una función que recibe el estado actual y retorna un objeto donde cada clave es un tipo de acción y su valor es el nuevo estado
- reducer: Busca si existe una transformación para el
action.typeen el objeto y la retorna, o devuelve el estado sin cambios si no existe
Ejemplo práctico: Sistema de autenticación
Vamos a construir un sistema de autenticación completo usando este patrón.
Paso 1: Define el estado inicial
const initialState = {
user: null,
isAuthenticated: false,
loading: false,
error: null,
token: null
};
Paso 2: Crea el reducerObject
const authReducerObject = (state, action) => ({
'LOGIN_START': {
...state,
loading: true,
error: null,
},
'LOGIN_SUCCESS': {
...state,
loading: false,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token,
error: null,
},
'LOGIN_ERROR': {
...state,
loading: false,
isAuthenticated: false,
user: null,
token: null,
error: action.payload,
},
'LOGOUT': {
...initialState,
},
'UPDATE_PROFILE': {
...state,
user: {
...state.user,
...action.payload,
},
},
'CLEAR_ERROR': {
...state,
error: null,
}
});
Paso 3: Crea el reducer
const authReducer = (state, action) => {
const newState = authReducerObject(state, action)[action.type];
return newState ? newState : state;
};
Paso 4: Usa el reducer en tu componente
import { useReducer } from 'react';
function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, initialState);
const login = async (email, password) => {
dispatch({ type: 'LOGIN_START' });
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
dispatch({
type: 'LOGIN_SUCCESS',
payload: data
});
} else {
dispatch({
type: 'LOGIN_ERROR',
payload: data.message
});
}
} catch (error) {
dispatch({
type: 'LOGIN_ERROR',
payload: 'Error de conexión'
});
}
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
const updateProfile = (updates) => {
dispatch({
type: 'UPDATE_PROFILE',
payload: updates
});
};
return (
<AuthContext.Provider value={{ state, login, logout, updateProfile }}>
{children}
</AuthContext.Provider>
);
}
Versión mejorada: Con payload dinámico
Podemos mejorar nuestro patrón para que sea aún más flexible:
const taskReducerObject = (state, action) => ({
'ADD_TASK': {
...state,
tasks: [...state.tasks, {
id: Date.now(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString()
}],
},
'TOGGLE_TASK': {
...state,
tasks: state.tasks.map(task =>
task.id === action.payload
? { ...task, completed: !task.completed }
: task
),
},
'DELETE_TASK': {
...state,
tasks: state.tasks.filter(task => task.id !== action.payload),
},
'EDIT_TASK': {
...state,
tasks: state.tasks.map(task =>
task.id === action.payload.id
? { ...task, text: action.payload.text }
: task
),
},
'SET_FILTER': {
...state,
filter: action.payload, // 'all', 'active', 'completed'
},
'CLEAR_COMPLETED': {
...state,
tasks: state.tasks.filter(task => !task.completed),
}
});
const taskReducer = (state, action) => {
return taskReducerObject(state, action)[action.type] || state;
};
Componente completo: To-Do List
import { useReducer } from 'react';
const initialState = {
tasks: [],
filter: 'all' // 'all', 'active', 'completed'
};
function TodoApp() {
const [state, dispatch] = useReducer(taskReducer, initialState);
const addTask = (text) => {
if (text.trim()) {
dispatch({ type: 'ADD_TASK', payload: text });
}
};
const toggleTask = (id) => {
dispatch({ type: 'TOGGLE_TASK', payload: id });
};
const deleteTask = (id) => {
dispatch({ type: 'DELETE_TASK', payload: id });
};
const editTask = (id, text) => {
dispatch({
type: 'EDIT_TASK',
payload: { id, text }
});
};
const setFilter = (filter) => {
dispatch({ type: 'SET_FILTER', payload: filter });
};
const clearCompleted = () => {
dispatch({ type: 'CLEAR_COMPLETED' });
};
const getFilteredTasks = () => {
switch (state.filter) {
case 'active':
return state.tasks.filter(task => !task.completed);
case 'completed':
return state.tasks.filter(task => task.completed);
default:
return state.tasks;
}
};
return (
<div className="todo-app">
<h1>Mi Lista de Tareas</h1>
<TaskInput onAdd={addTask} />
<FilterButtons
currentFilter={state.filter}
onFilterChange={setFilter}
/>
<TaskList
tasks={getFilteredTasks()}
onToggle={toggleTask}
onDelete={deleteTask}
onEdit={editTask}
/>
<div className="footer">
<span>{state.tasks.filter(t => !t.completed).length} tareas pendientes</span>
<button onClick={clearCompleted}>
Limpiar completadas
</button>
</div>
</div>
);
}
Ventajas del Reducer Object Pattern
- Más limpio: Menos boilerplate y sintaxis más clara
- Más legible: Cada acción es un objeto con su transformación
- Fácil de extender: Agregar nuevas acciones es simplemente agregar una nueva propiedad
- Menos errores: No hay que preocuparse por
breakoreturnolvidados - Mejor organización: Todas las transformaciones están en un solo lugar
Comparación: Switch vs Object
Con Switch (Tradicional)
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
}
Con Object Pattern (Moderno)
const reducerObject = (state) => ({
'INCREMENT': { ...state, count: state.count + 1 },
'DECREMENT': { ...state, count: state.count - 1 },
'RESET': { ...state, count: 0 }
});
const reducer = (state, action) =>
reducerObject(state)[action.type] || state;
Cuándo usar cada patrón
Usa Switch cuando:
- Necesitas lógica compleja dentro de cada caso
- Tienes transformaciones que requieren múltiples pasos
- El equipo está más familiarizado con este patrón
Usa Object Pattern cuando:
- Tienes muchas acciones simples
- Quieres código más conciso
- Buscas mejor legibilidad
- Las transformaciones son directas
Tips y mejores prácticas
1. Combina con constantes
const ACTIONS = {
ADD_TASK: 'ADD_TASK',
DELETE_TASK: 'DELETE_TASK',
TOGGLE_TASK: 'TOGGLE_TASK'
};
const reducerObject = (state, action) => ({
[ACTIONS.ADD_TASK]: {
...state,
tasks: [...state.tasks, action.payload]
},
// ...
});
2. Extrae la lógica compleja
const addTaskLogic = (state, payload) => ({
...state,
tasks: [...state.tasks, {
id: Date.now(),
text: payload,
completed: false
}]
});
const reducerObject = (state, action) => ({
'ADD_TASK': addTaskLogic(state, action.payload),
// ...
});
3. Usa default values
const reducer = (state = initialState, action) => {
return reducerObject(state, action)[action.type] ?? state;
};
Conclusión
El Reducer Object Pattern es una alternativa elegante y moderna al patrón tradicional de switch. No reemplaza completamente el uso de switch – cada uno tiene su lugar – pero para muchos casos de uso, especialmente cuando tienes múltiples acciones simples, este patrón puede hacer tu código más limpio y mantenible.
Recuerda que lo más importante no es qué patrón uses, sino que tu código sea consistente, legible y fácil de mantener para todo el equipo. Experimenta con ambos enfoques y elige el que mejor se adapte a tu caso de uso específico.
Referencias
- useReducer en React: La guía completa – Artículo anterior donde explicamos los fundamentos de useReducer