Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Refactorización de Lógica de Reductores Mediante Descomposición Funcional y Composición de Reductores
Puede ser útil ver ejemplos de cómo son los diferentes tipos de funciones sub-reductoras y cómo encajan entre sí. Veamos una demostración de cómo una función reductora única y grande puede refactorizarse en una composición de varias funciones más pequeñas.
Nota: este ejemplo está escrito deliberadamente de forma detallada para ilustrar los conceptos y el proceso de refactorización, en lugar de ser un código perfectamente conciso.
Reductor Inicial
Supongamos que nuestro reductor inicial luce así:
const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return Object.assign({}, state, {
visibilityFilter: action.filter
})
}
case 'ADD_TODO': {
return Object.assign({}, state, {
todos: state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
case 'TOGGLE_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
completed: !todo.completed
})
})
})
}
case 'EDIT_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
text: action.text
})
})
})
}
default:
return state
}
}
Esta función es bastante corta, pero ya empieza a ser excesivamente compleja. Estamos manejando dos áreas de preocupación diferentes (filtrado vs gestión de nuestra lista de tareas), el anidamiento dificulta la lectura de la lógica de actualización, y no queda del todo claro qué está ocurriendo en todas partes.
Extracción de Funciones Utilitarias
Un buen primer paso podría ser extraer una función utilitaria que devuelva un nuevo objeto con campos actualizados. También hay un patrón repetido al intentar actualizar un elemento específico en un array que podríamos extraer a una función:
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return updateObject(state, { visibilityFilter: action.filter })
}
case 'ADD_TODO': {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
case 'TOGGLE_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
case 'EDIT_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
default:
return state
}
}
Esto redujo la duplicación y facilitó un poco la lectura.
Extracción de Reductores de Casos
A continuación, podemos dividir cada caso específico en su propia función:
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(state, action) {
return updateObject(state, { visibilityFilter: action.filter })
}
function addTodo(state, action) {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
function toggleTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
function editTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(state, action)
case 'ADD_TODO':
return addTodo(state, action)
case 'TOGGLE_TODO':
return toggleTodo(state, action)
case 'EDIT_TODO':
return editTodo(state, action)
default:
return state
}
}
Ahora queda muy claro qué está ocurriendo en cada caso. También podemos empezar a ver que emergen algunos patrones.
Separación del Manejo de Datos por Dominio
Nuestro reductor de aplicación todavía conoce todos los diferentes casos de nuestra aplicación. Intentemos separar las cosas para que la lógica de filtrado y la lógica de tareas estén separadas:
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}
function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(todosState, action)
case 'TOGGLE_TODO':
return toggleTodo(todosState, action)
case 'EDIT_TODO':
return editTodo(todosState, action)
default:
return todosState
}
}
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
Observa que, como los dos reductores de "porción del estado" ahora reciben solo su propia parte del estado completo como argumentos, ya no necesitan devolver objetos de estado anidados complejos, y como resultado ahora son más simples.
Reducción de Código Repetitivo
Ya casi terminamos. Como a mucha gente no le gustan las declaraciones switch, es muy común usar una función que crea una tabla de búsqueda de tipos de acción a funciones de caso. Usaremos la función createReducer descrita en Reducción de Código Repetitivo:
// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// Omitted
function setVisibilityFilter(visibilityState, action) {}
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Omitted
function addTodo(todosState, action) {}
function toggleTodo(todosState, action) {}
function editTodo(todosState, action) {}
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
Combinación de Reductores por Porción
Como último paso, ahora podemos usar la utilidad incorporada de Redux combineReducers para manejar la lógica de "porción del estado" en nuestro reductor de aplicación de nivel superior. Aquí está el resultado final:
// Reusable utility functions
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// Handler for a specific case ("case reducer")
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}
// Handler for an entire slice of state ("slice reducer")
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Case reducer
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
// Case reducer
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
// Case reducer
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
// Slice reducer
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
// "Root reducer"
const appReducer = combineReducers({
visibilityFilter: visibilityReducer,
todos: todosReducer
})
Ahora tenemos ejemplos de varios tipos de funciones reductoras separadas: utilidades auxiliares como updateObject y createReducer, manejadores para casos específicos como setVisibilityFilter y addTodo, y manejadores de porción del estado como visibilityReducer y todosReducer. También podemos ver que appReducer es un ejemplo de un "reductor raíz".
Aunque el resultado final en este ejemplo es notablemente más largo que la versión original, esto se debe principalmente a la extracción de las funciones utilitarias, la adición de comentarios y cierta deliberada verbosidad en aras de la claridad, como declaraciones de retorno separadas. Al observar cada función individualmente, la cantidad de responsabilidad ahora es menor y la intención es más clara. Además, en una aplicación real, estas funciones probablemente se separarían en archivos distintos como reducerUtilities.js, visibilityReducer.js, todosReducer.js y rootReducer.js.