Redux — отличный инструмент для управления сложными состояниями приложений, но он становится таким замечательным только после того, как вы преодолеете кривую обучения. В зависимости от вашего первого знакомства с концепцией Redux понимание идеи может быть довольно запутанным, поэтому иногда некоторые просто отвергают ее как слишком сложную. Для того, чтобы в полной мере оценить, как Redux может на самом деле облегчить их жизнь, важно сначала хорошо понять, что такое «редьюсер».

редуктор
сущ. [Информатика] Заноза в заднице, представленная Redux.
см. также: Редукс

В каком-то смысле эта шутка — полуправда, потому что само определение редюсера может сбивать с толку. Итак, давайте рассмотрим эту идею и постараемся сделать ее максимально простой.

Если вы знаете Javascript, вы, вероятно, знаете или, по крайней мере, слышали о методе Array reduce(). Что-то вроде этого выглядело бы знакомо:

array.reduce((accumulator, item) => accumulator + item)

Массив reduce() принимает функцию в качестве своего аргумента и выполняет эту функцию для каждого элемента массива, при этом каждая итерация сохраняет результат в «накопитель», который затем передается на следующую итерацию, в конечном итоге становясь единственным окончательным значением. Эта функция, переданная reduce(), на самом деле называется — угадайте, как — reducer, и редуктор Redux на самом деле является одной из таких функций, как мы увидим позже.

Чтобы проиллюстрировать логику с помощью кода (разве вы не любите читать код?), приведенный выше пример array.reduce() можно перевести примерно так:

const reducer = (accumulator, item) => accumulator + item;
let result = 0;
for (let i = 0; i < array.length; i++) {
  result = reducer(result, array[i]);
}
return result;

В этом примере у нас фактически есть функция sum(), где функция редуктора действует как функция промежуточного итога (или, точнее, «всего на данный момент»), и после выполнения редуктора для всех элементов в массиве чисел будет дать общую сумму.

Но это был только пример. В общем, функция-редуктор может быть любой, которая принимает текущее значение (т. е. накопитель) плюс какое-то новое значение (т. е. следующий элемент в массиве), а затем возвращает результат некоторых вычислений, выполненных с использованием этих двух значений (текущего и нового). Обратите внимание, что мы говорим о редюсере в общем смысле, а не о редюсере Redux (обещаю, мы скоро к этому вернемся).

Теперь давайте рассмотрим более практический пример. Допустим, нам нужна функция редуктора, которая добавляет книги в каталог. Информация о книге передается как объект (пары ключ-значение), и редуктор добавляет ее в список с ключами.

const reducer = (catalog, book) => {
  return { ...catalog, [book.id]: book };
}

Вы также можете представить, что наш исходный массив (тот, в чей метод reduce() мы передадим эту функцию редуктора) должен быть массивом объектов информации о книге.

Это достаточно легко, не так ли? Но что, если нам также нужно, чтобы наш редьюсер мог делать другие вещи с каталогом книг, например удалять из него книги. Мы понимаем, что кроме аккумулятора единственным другим аргументом, который мы передаем редьюсеру, является наш «предмет», которым в приведенном выше примере является «книга». Но чтобы иметь возможность поддерживать другие действия в каталоге, нам нужно передать другой тип аргумента «элемент». В этом случае это может быть что-то вроде «действие», как в этом примере:

let actionAddBook = {
  type: 'ADD BOOK',
  data: book
}
let actionRemoveBook = {
  type: 'REMOVE BOOK',
  data: book.id
}

Итак, наш редюсер будет выглядеть так:

const reducer = (catalog, action) => {
  switch (action.type) {
    case 'ADD BOOK':
      return { ...catalog, [action.data.id]: action.data }
    case 'REMOVE BOOK':
      let updatedCatalog = { ...catalog };
      delete updatedCatalog[action.data];
      return updatedCatalog;
    default:
      return catalog;
  }
}

В этом случае наш исходный массив, который мы будем перебирать методом reduce(), должен быть массивом действий.

Знаете что, мы только что написали настоящий Redux-редуктор! Но давайте обсудим здесь несколько ключевых моментов…

Redux предназначен для управления состоянием приложения. Все состояние вашего приложения — представьте его как один большой объект данных — разделено на несколько управляемых частей, каждая из которых имеет свой редюсер, позволяющий вносить изменения в это состояние. В нашем примере такой частью состояния является наш книжный каталог.

Как правило, редуктор Redux работает следующим образом: он берет действие из потока отправленных действий, затем на основе распознанного типа действия возвращает новое состояние, полученное из некоторых данных (также называется полезной нагрузкой), указанным в действии. Поэтому мы можем сказать, что общая форма редуктора Redux такова:

(state, action) => newState

Обратите внимание, что он возвращает новый newState вместо того, чтобы напрямую манипулировать state (например, отправка массива или мутация объекта). Она считается чистой функцией, поскольку обладает следующими характеристиками:

  1. При одном и том же вводе он всегда возвращает один и тот же вывод.
  2. Он никоим образом не изменяет ввод и не вызывает никаких других побочных эффектов.

Так вот. У нас есть работающий Редуктор Redux. Давайте закончим это, переписав наш редьюсер, чтобы он больше походил на то, как это делается в типичном коде Redux…

const catalogReducer = (state, action) => {
  switch (action.type) {
    case 'ADD BOOK':
      return { ...state, [action.data.id]: action.data }
    case 'REMOVE BOOK':
      let newState = { ...state };
      delete newState[action.data];
      return newState;
    default:
      return state;
  }
}