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
(например, отправка массива или мутация объекта). Она считается чистой функцией, поскольку обладает следующими характеристиками:
- При одном и том же вводе он всегда возвращает один и тот же вывод.
- Он никоим образом не изменяет ввод и не вызывает никаких других побочных эффектов.
Так вот. У нас есть работающий Редуктор 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; } }