Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
Рефакторинг логики редьюсеров с помощью функциональной декомпозиции и композиции
Полезно рассмотреть примеры того, как выглядят разные типы под-редьюсеров и как они взаимодействуют. Давайте разберём, как можно преобразовать один большой редьюсер в композицию из нескольких небольших функций.
Примечание: этот пример намеренно написан в подробном стиле, чтобы проиллюстрировать концепции и процесс рефакторинга, а не показать максимально лаконичный код.
Исходный редьюсер
Предположим, наш изначальный редьюсер выглядит так:
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
}
}
Эта функция довольно короткая, но уже становится излишне сложной. Мы имеем дело с двумя разными областями ответственности (фильтрация vs управление списком задач), вложенность затрудняет чтение логики обновления, и не везде понятно, что происходит.
Выделение вспомогательных функций
Хорошим первым шагом будет создание утилитной функции для возврата нового объекта с обновлёнными полями. Также можно выделить повторяющийся паттерн обновления конкретного элемента массива:
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
}
}
Это уменьшило дублирование кода и сделало его немного читаемее.
Выделение обработчиков кейсов
Далее можно разделить каждый конкретный кейс на отдельную функцию:
// 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
}
}
Теперь стало очень понятно, что происходит в каждом случае. Также начинают проявляться некоторые паттерны.
Разделение обработки данных по доменам
Наш корневой редьюсер по-прежнему знает обо всех кейсах приложения. Попробуем разделить логику фильтрации и обработки задач:
// 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)
}
}
Обратите внимание: поскольку редьюсеры "срезов состояния" теперь получают только свою часть общего состояния в качестве аргументов, им больше не нужно возвращать сложные вложенные объекты состояния, что делает их проще.
Уменьшение шаблонного кода
Мы почти закончили. Поскольку многим не нравятся switch-операторы, часто используют функцию, создающую таблицу соответствия типов действий и функций-обработчиков. Мы воспользуемся функцией createReducer, описанной в разделе Уменьшение шаблонного кода:
// 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)
}
}
Комбинирование редьюсеров по срезам
На последнем шаге мы можем использовать встроенную утилиту Redux combineReducers для обработки логики "срезов состояния" в корневом редьюсере. Вот финальный результат:
// 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
})
Теперь у нас есть примеры нескольких типов разделённых редьюсеров: вспомогательные утилиты вроде updateObject и createReducer, обработчики конкретных кейсов вроде setVisibilityFilter и addTodo, а также обработчики срезов состояния вроде visibilityReducer и todosReducer. Мы также видим, что appReducer является примером "корневого редьюсера".
Хотя финальный результат в этом примере заметно длиннее исходной версии, это в основном связано с выделением утилитных функций, добавлением комментариев и намеренной детализацией для ясности (например, отдельные return-операторы). Если смотреть на каждую функцию по отдельности, объём ответственности стал меньше, а намерения — понятнее. В реальном приложении эти функции, вероятно, были бы вынесены в отдельные файлы: reducerUtilities.js, visibilityReducer.js, todosReducer.js и rootReducer.js.