VOLVER

Refactoriza TodoMVC con Redux Starter Kit

7 min de lectura

He trabajado con React durante más de dos años. Empecé en un proyecto grande que ya usaba Redux. Sumergirme en tanto código existente fue abrumador, especialmente con un framework desconocido. Con el tiempo, me sentí cómodo y experimentado.

Recientemente descubrí Redux Starter Kit del equipo de Redux. Este conjunto de herramientas simplifica el trabajo con Redux. Una herramienta, createReducer, sigue un patrón que uso desde hace tiempo. Reduce código repetitivo y acelera el desarrollo, especialmente en proyectos nuevos.

Para aprender estas herramientas, migré una base de código existente con Redux. Como ejemplo, elegí el omnipresente TodoMVC, específicamente la versión del repositorio de Redux.

Punto de partida

La app tiene dos reducers principales: visibilityFilter y todos. Cada uno tiene sus propias acciones, creadores de acciones y selectores.

Filtro de Visibilidad

Empecé con el reducer más simple, luego pasé al más complejo.

Reducer

El reducer del ejemplo de Redux ya es simple y claro.

// reducers/visibilityFilter.js
import { SET_VISIBILITY_FILTER } from "../constants/ActionTypes";
import { SHOW_ALL } from "../constants/TodoFilters";
export default (state = SHOW_ALL, action) => {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
};

Redux Starter Kit proporciona createReducer para crear reducers. Como mencioné, ya uso este patrón y me resulta efectivo.

En lugar de crear un reducer con una sentencia switch case, pasas el estado inicial como primer parámetro y un objeto que mapea tipos de acción a funciones reducer ((state, action) => { /* código del reducer */}).

Reduce código repetitivo y maneja automáticamente el caso default con return state. El mayor beneficio: mejor legibilidad.

Aquí está el reducer del filtro de visibilidad usando createReducer:

// reducers/visibilityFilter.js
import { createReducer } from "redux-starter-kit";
import { SET_VISIBILITY_FILTER } from "../constants/ActionTypes";
import { SHOW_ALL } from "../constants/TodoFilters";
export default createReducer(SHOW_ALL, {
[SET_VISIBILITY_FILTER]: (state, action) => action.filter,
});

Creadores de acciones

Ahora las acciones. El filtro de visibilidad tiene una acción, SET_VISIBILITY_FILTER, con un creador simple:

// actions/index.js
import * as types from "../constants/ActionTypes";
/* ... Otras acciones ...*/
export const setVisibilityFilter = (filter) => ({
type: types.SET_VISIBILITY_FILTER,
filter,
});

El conjunto de herramientas proporciona createAction, que toma solo el tipo de acción como parámetro y devuelve un creador de acciones.

// actions/index.js
import * as types from "../constants/ActionTypes";
/* ... Otras acciones ...*/
export const setVisibilityFilter = createAction(types.SET_VISIBILITY_FILTER);

Este creador de acciones acepta parámetros opcionales. Cualquier argumento se convierte en el payload de la acción:

const setVisibilityFilter = createAction("SET_VISIBILITY_FILTER");
let action = setVisibilityFilter();
// { type: 'SET_VISIBILITY_FILTER' }
action = setVisibilityFilter("SHOW_COMPLETED");
// devuelve { type: 'SET_VISIBILITY_FILTER', payload: 'SHOW_COMPLETED' }
setVisibilityFilter.toString();
// 'SET_VISIBILITY_FILTER'

Ahora el filtro usa la clave payload en lugar de filter. Esto requiere un pequeño cambio en el reducer:

// reducers/visibilityFilter.js
import { createReducer } from "redux-starter-kit";
import { SET_VISIBILITY_FILTER } from "../constants/ActionTypes";
import { SHOW_ALL } from "../constants/TodoFilters";
export default createReducer(SHOW_ALL, {
[SET_VISIBILITY_FILTER]: (state, action) => action.payload,
});

Selectores

Los selectores son una de las mejores elecciones al trabajar con React. Permiten refactorizar la estructura del estado sin cambiar cada componente que lo consume.

El selector del filtro de visibilidad es directo:

// selectors/index.js
const getVisibilityFilter = (state) => state.visibilityFilter;
/* ... Otros selectores ...*/

Usar createSelector añade un poco más de código, pero la recompensa llega pronto. Sigue leyendo.

// selectors/index.js
import { createSelector } from "redux-starter-kit";
const getVisibilityFilter = createSelector(["visibilityFilter"]);
/* ... Otros selectores ...*/

Slices

Hasta ahora, hemos reemplazado funciones simples con otras más simples usando varios creadores. Ahora viene el verdadero poder del conjunto de herramientas: createSlice.

createSlice acepta un estado inicial, funciones reducer y un nombre de slice opcional. Genera automáticamente creadores de acciones, tipos de acciones y selectores.

Ahora podemos descartar todo el código anterior.

Crear un slice para el filtro de visibilidad es limpio y elimina código repetitivo significativo.

// ducks/visibilityFilter.js
import { createSlice } from "redux-starter-kit";
export default createSlice({
slice: "visibilityFilter",
initialState: SHOW_ALL,
reducers: {
setVisibilityFilter: (state, action) => action.payload,
},
});

El resultado es un solo objeto con todo lo necesario para trabajar con Redux:

const reducer = combineReducers({
visibilityFilter: visibilityFilter.reducer,
});
const store = createStore(reducer);
store.dispatch(visibilityFilter.actions.setVisibilityFilter(SHOW_COMPLETED));
// -> { visibilityFilter: 'SHOW_COMPLETED' }
const state = store.getState();
console.log(visibilityFilter.selectors.getVisibilityFilter(state));
// -> SHOW_COMPLETED

Ver todos los cambios hasta ahora en este commit.

Todos

El reducer de todos es más complejo, así que explicaré el resultado final en lugar de cada paso. Ver el código completo aquí.

Primero, definir el estado inicial:

// ducks/todos.js
const initialState = [
{
text: "Use Redux",
completed: false,
id: 0,
},
];

Para mejorar la legibilidad, extraje cada acción del reducer en su propia función:

// ducks/todos.js
const addTodo = (state, action) => [
...state,
{
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.payload.text,
},
];
const deleteTodo = (state, action) =>
state.filter((todo) => todo.id !== action.payload.id);
const editTodo = (state, action) =>
state.map((todo) =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo,
);
const completeTodo = (state, action) =>
state.map((todo) =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo,
);
const completeAllTodos = (state) => {
const areAllMarked = state.every((todo) => todo.completed);
return state.map((todo) => ({
...todo,
completed: !areAllMarked,
}));
};
const clearCompleted = (state) =>
state.filter((todo) => todo.completed === false);

Ahora combínalos en un slice:

// ducks/todos.js
const todos = createSlice({
slice: "todos",
initialState,
reducers: {
add: addTodo,
delete: deleteTodo,
edit: editTodo,
complete: completeTodo,
completeAll: completeAllTodos,
clearCompleted: clearCompleted,
},
});

Por defecto, los selectores de createSlice simplemente devuelven valores del estado (ej: todos.selectors.getTodos). Esta aplicación necesita selectores más complejos.

Por ejemplo, getVisibleTodos necesita el filtro de visibilidad y los todos. createSelector toma un array de selectores (o rutas del estado) como primer parámetro y una función implementando la lógica de selección como segundo.

// ducks/todos.js
const { getVisibilityFilter } = visibilityFilter.selectors;
todos.selectors.getVisibleTodos = createSelector(
[getVisibilityFilter, todos.selectors.getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case SHOW_ALL:
return todos;
case SHOW_COMPLETED:
return todos.filter((t) => t.completed);
case SHOW_ACTIVE:
return todos.filter((t) => !t.completed);
default:
throw new Error("Filtro desconocido: " + visibilityFilter);
}
},
);
todos.selectors.getCompletedTodoCount = createSelector(
[todos.selectors.getTodos],
(todos) =>
todos.reduce((count, todo) => (todo.completed ? count + 1 : count), 0),
);

Añadí los nuevos selectores al objeto todos.selectors, manteniendo todos los selectores en un solo lugar.

Crear Store

La librería también proporciona configureStore y getDefaultMiddleware.

configureStore envuelve el createStore de Redux. Ofrece la misma funcionalidad con una API más limpia--habilitar herramientas de desarrollador requiere solo un booleano.

getDefaultMiddleware devuelve [immutableStateInvariant, thunk, serializableStateInvariant] en desarrollo y [thunk] en producción.

  • redux-immutable-state-invariant: Detecta mutaciones en reducers durante el dispatch y entre dispatches (en selectores o componentes).
  • serializable-state-invariant-middleware: Comprueba estado y acciones para valores no serializables como funciones y Promesas.
// store.js
import { configureStore, getDefaultMiddleware } from "redux-starter-kit";
import { combineReducers } from "redux";
import { visibilityFilter, todos } from "./ducks";
const preloadedState = {
todos: [
{
text: "Use Redux",
completed: false,
id: 0,
},
],
};
const reducer = combineReducers({
todos: todos.reducer,
visibilityFilter: visibilityFilter.reducer,
});
const middleware = [...getDefaultMiddleware()];
export const store = configureStore({
reducer,
middleware,
devTools: process.env.NODE_ENV !== "production",
preloadedState,
});

Pensamientos finales

Redux Starter Kit reduce código repetitivo, haciendo el código más limpio y fácil de entender. También acelera el desarrollo.

Código Fuente: https://github.com/magarcia/todomvc-redux-starter-kit