Шаблоны неизменяемых обновлений
Статьи, перечисленные в Основные понятия#Управление неизменяемыми данными, предоставляют множество хороших примеров выполнения основных операций обновления неизменяемым образом, таких как обновление поля в объекте или добавление элемента в конец массива. Однако редукторы часто должны использовать эти базовые операции в сочетании для выполнения более сложных задач. Вот несколько примеров для некоторых из наиболее распространенных задач, которые вам, возможно, придется реализовать.
Обновление вложенных объектов
Ключ к обновлению вложенных данных заключается в том, что каждый уровень вложенности должен быть скопирован и обновлен соответствующим образом. Это часто является сложным понятием для тех, кто изучает Redux, и существуют некоторые специфические проблемы, которые часто возникают при попытке обновить вложенные объекты. Они приводят к случайному прямому изменению, и их следует избегать.
Ошибка №1: Новые переменные, указывающие на те же объекты
Определение новой переменной не создает новый фактический объект — она только создает другую ссылку на тот же объект. Примером этой ошибки является:
function updateNestedState(state, action) {
let nestedState = state.nestedState;
// ERROR: this directly modifies the existing object reference - don't do this!
nestedState.nestedField = action.data;
return {
...state,
nestedState
};
}
Эта функция правильно возвращает поверхностную копию объекта состояния верхнего уровня, но поскольку переменная nestedState по-прежнему указывала на существующий объект, состояние было изменено напрямую.
Ошибка №2: Только создание поверхностной копии одного уровня
Другая распространенная версия этой ошибки выглядит так:
function updateNestedState(state, action) {
// Problem: this only does a shallow copy!
let newState = {...state};
// ERROR: nestedState is still the same object!
newState.nestedState.nestedField = action.data;
return newState;
}
Создание поверхностной копии верхнего уровня недостаточно — объект nestedState также должен быть скопирован.
Правильный подход: Копирование всех уровней вложенных данных
К сожалению, процесс правильного применения неизменяемых обновлений к глубоко вложенному состоянию может легко стать громоздким и трудночитаемым. Вот пример обновления state.first.second[someId].fourth:
function updateVeryNestedField(state, action) {
return {
...state,
first : {
...state.first,
second : {
...state.first.second,
[action.someId] : {
...state.first.second[action.someId],
fourth : action.someValue
}
}
}
}
}
Очевидно, каждый уровень вложенности делает это труднее читать и увеличивает вероятность ошибок. Это одна из нескольких причин, по которым рекомендуется держать состояние плоским и насколько это возможно компоновать редукторы.
Вставка и удаление элементов в массивах
Обычно содержимое массива JavaScript изменяется с помощью мутационных функций, таких как push, unshift, и splice. Поскольку мы не хотим изменять состояние напрямую в редукторах, их обычно следует избегать. Из-за этого вы можете увидеть поведение "вставки" или "удаления", написанное так:
function insertItem(array, action) {
return [
...array.slice(0, action.index),
action.item,
...array.slice(action.index)
]
}
function removeItem(array, action) {
return [
...array.slice(0, action.index),
...array.slice(action.index + 1)
];
}
Однако помните, что ключевым моментом является то, что исходная ссылка в памяти не изменяется. Пока мы создаем копию, мы можем безопасно изменить копию. Обратите внимание, что это верно как для массивов, так и для объектов, но вложенные значения все равно должны обновляться по тем же правилам.
Это означает, что мы также можем написать функции вставки и удаления так:
function insertItem(array, action) {
let newArray = array.slice();
newArray.splice(action.index, 0, action.item);
return newArray;
}
function removeItem(array, action) {
let newArray = array.slice();
newArray.splice(action.index, 1);
return newArray;
}
Функция удаления также может быть реализована так:
function removeItem(array, action) {
return array.filter( (item, index) => index !== action.index);
}
Обновление элемента в массиве
Обновление одного элемента в массиве можно выполнить, используя Array.map, вернув новое значение для элемента, который мы хотим обновить, и вернув существующие значения для всех остальных элементов:
function updateObjectInArray(array, action) {
return array.map( (item, index) => {
if(index !== action.index) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
};
});
}
Библиотеки полезных программ для неизменяемых обновлений
Поскольку написание кода неизменяемых обновлений может стать утомительным, существует ряд библиотек полезных программ, которые пытаются абстрагировать этот процесс. Эти библиотеки отличаются API и использованием, но все они пытаются предоставить более короткий и лаконичный способ написания этих обновлений. Некоторые из них, например, dot-prop-immutable, используют строковые пути для команд:
state = dotProp.set(state, `todos.${index}.complete`, true)
Другие, такие как immutability-helper (ветвь ныне устаревшего дополнения React Immutability Helpers), используют вложенные значения и вспомогательные функции:
var collection = [1, 2, {a: [12, 17, 15]}];
var newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
Они могут предоставить полезную альтернативу написанию ручного кода неизменяемых обновлений.
Список многих полезных программ для неизменяемых обновлений можно найти в разделе Неизменяемые данные#Полезные программы для неизменяемых обновлений каталога Redux Addons Catalog.
© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html