Refactoriza TodoMVC con Redux Starter Kit
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.jsimport { 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.jsimport { 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.jsimport * 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.jsimport * 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.jsimport { 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.jsconst 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.jsimport { 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.jsimport { 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.jsconst 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.jsconst 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.jsconst 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.jsconst { 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.jsimport { 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