Редукторы в Redux - это место, где функциональное программирование и концепция неизменяемости очень заметны. Неизменяемость в Javascript неестественна, поэтому некоторые прибегают к библиотекам вроде Immutable. Мне? Я предпочитаю собственные массивы, объекты и примитивы структурам данных, определяемым библиотекой, поэтому я прибегаю к различным шаблонам ES6 для реализации неизменяемости.

Моя проблема в том, что неизменяемость объекта довольно проста, но неизменность массива сложнее. Чтобы понять проблему и ее контекст, давайте подведем итоги проблемы неизменяемости Redux.

Редукторы и неизменяемость Redux

В Redux редюсер - это функция, которая принимает «состояние» приложения, «действие» и возвращает новое состояние. Приведем пример:

const intReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return state + action.delta
    case 'decrement':
      return state - action.delta
  }
}

Примеры использования:

console.log(intReducer(5, {type: 'increment', delta: 2})) // 7
console.log(intReducer(5, {type: 'decrement', delta: 2})) // 3

Давайте попробуем более сложный пример, где состояние - это объект:

const personReducer = (state, action) => {
  switch (action.type) {
    case 'upperCaseFirstName':
      return Object.assign({}, state, 
       {firstName: state.firstName.upper()})
    case 'upperCaseLastName':
      return Object.assign({}, state, 
        {lastName: state.lastName.toUpper()})
  }
}
const person = {firstName: 'Gil', lastName: 'Tayar'}
console.log(personReducer(person, {type: 'upperCaseFirstName'})) 
      // {fistName: 'GIL', lastName: 'Tayar'}

Обратите внимание на использование Object.assign для создания нового состояния, а не на изменение предыдущего. Object.assign идеально подходит для этого, и когда JS получит новый оператор распространения для объектов, этот метод будет еще лучше:

{...state, firstName: state.firstName.upper}

Неизменяемость массивов

Но что, если мое состояние - это массив? Возьмем пример прямо из TodoMVC:

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'markDone':
      const newState = [...state] // clone the array
      newState[action.index].done = true
      return newState
  }
}
const todos = [{title: 'laundry', done: false}, {title: 'dishes', done: false}]
console.log(todoReducer(todos, {type: 'markDone', index: 1}))
  // [{title: 'laundry', done: false}, 
  //  {title: 'dishes', done: true}]

Ой. Больно! Вместо красивого однострочника у меня есть процедурный и нефункциональный код. В частности, я смотрю на это:

const newState = [...state] // clone the array
newState[action.index].done = true
return newState

Я мог сделать это:

return [...state.slice(0, action.index), 
        Object.assign({}, state[action.index], {done: true}),
        ...state.slice(action.index + 1)]

Я использую Array.slice и оператор распространения, но уверен, что лекарство не хуже болезни!

Решение

Я читал электронные письма из списка esdiscuss и нашел такой вопрос:





Кроме того: если вы интересуетесь будущим Javascript или хотите глубже понять Javascript, то список рассылки es-Discussion для вас. [Обновление (декабрь 2017 г.): к сожалению, как и многие другие полезные вещи в Интернете, этот список рассылки стал немного утомительным. Много троллей, много дискуссий, ведущих в никуда. Тем не менее, у него все еще есть хорошая информация, и я еще не отписался!]

Джереми Мартин хотел найти способ установить значение в массиве неизменяемо и предложил будущую функцию, которая включила бы эту функцию.

Бац! В точности то, что я искал. Но на этот вопрос быстро был дан ответ, который (i) поразил меня и (ii) заставил меня задуматься: «Почему же теперь я не подумал об этом?». Давайте воспользуемся методом и напишем код, который неизменяемо изменяет 3-й элемент в массиве:

const array = ['a', 'b', 'c', 'd']
console.log(Object.assign([...array], {2: 'x'}))
  // ['a','b','x','d']

Хм? Object.assign? И мы используем его в массиве? Давайте на секунду проигнорируем эту странность и разберемся, что мы здесь делаем:

Сначала мы клонируем массив: [...array]. Затем мы заменяем объект , который является массивом, на {2: 'x'}. И о чудо - работает.

Массивы - это объекты

Почему это работает? Потому что массивы - это объекты. В чем-то массив ['a', 'b'] похож на объект

{0: 'a', 1: 'b', length: 2}

Например, этот код будет работать для обоих:

for (let i = 0; i < a.length; ++i)
  console.log(a[i])

Кроме того, этот код будет работать для обоих:

a.foo = 'hi'
console.log(a.foo) // hi

Но объекты могут быть только массивами

Итак, массив является объектом. Но может ли объект быть массивом?

Неа. Например:

console.log(a.length) // 2 
a[2] = 'c'
console.log(a.length) // ???

Если a - массив, a.length будет обновлен с 2 до 3. Но попробуйте это сделать с созданным нами выше объектом, и a.length останется 2.

Также этот код не будет работать для объекта:

for (let i in a)
  console.log(a[i])

И еще много всего кода не будет работать. Поскольку массив является объектом, но объект может быть только подобным массиву. Он может иметь те же поля, что и массив, но не имеет поведения Array .

Почему решение работает

Вернемся к нашему примеру:

const array = ['a', 'b', 'c', 'd']
console.log(Object.assign([...array], {2: 'x'}))

Поскольку мы можем думать о массиве как об объекте, ключи которого являются индексами массива, мы можем использовать Object.assign для установки этих «ключей», что мы и делаем здесь.

Вернуться к нашему редуктору

Если мы хотим написать функцию, которая неизменно устанавливает значение индекса:

const setArrayImmutable = (arr, i, value) => 
  Object.assign([...arr], {[i]: value})

Обратите внимание на использование синтаксиса ES6 для определения ключа поля с помощью expression[i]: value.

Теперь мы можем сделать что-то подобное в нашем редукторе:

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'markDone':
     return Object.assign(
       [...state], 
       {[action.index]:
         Object.assign({}, state[action.index], {done: true}))

  }
}

И в этом трюк. Теперь неизменяемое изменение элемента массива так же просто, как неизменное изменение свойства объекта.