Что такое редуктор в JavaScript? Полное введение с примерами

Редукторы - это ключевая концепция, которую нужно понять, когда вы переходите к созданию более крупных приложений JavaScript? Вот полное введение в использование и понимание редукторов.

Для большинства приложений JavaScript редуктор является важной концепцией, которая помогает нам управлять состоянием приложения.

Он используется практически во всех библиотеках или фреймворках JavaScript, React, Angular и Vue, особенно в библиотеках управления состоянием Redux и ngrx. Это важно понимать, чтобы понять, как управлять состоянием в средних и крупных приложениях.

Что такое редуктор?

Редуктор - это очень простая идея, и вам будет легко понять это, потому что, в двух словах, это всего лишь простая функция JS.

Редуктор - это функция, которая принимает два аргумента - текущее состояние и действие - и возвращает на основе обоих аргументов новое состояние.

Мы можем выразить идею в одной строке, как почти верную функцию:

const reducer = (state, action) => newState;

Давайте рассмотрим очень простой пример, в котором нам нужно управлять некоторыми данными, скажем, в нашем приложении есть счетчик, где мы можем увеличивать или уменьшать число на 1. Итак, давайте возьмем наш редуктор и назовем его counterReducer. Эта функция будет выполняться для обновления состояния всякий раз, когда пользователь хочет отсчитывать вверх или вниз. В результате в теле функции мы просто хотим вернуть состояние + 1:

function counterReducer(state, action) {
  return state + 1;
}

Так что на данный момент наш счетчик увеличивается каждый раз только на 1.

Если это выглядит запутанным, мы можем переименовать state в count:

function counterReducer(count, action) {
  return count + 1;
}

Допустим, начальное состояние равно 0, после запуска мы ожидаем, что результат будет 1. И это:

counterReducer(0) === 1; // true

Так что же такого особенного в этом и зачем нам это использовать?

Прежде всего, редукторы особенные, потому что они предсказуемы. Другими словами, они являются реальным примером чистых функций, которые при определенных входных данных всегда будут иметь один и тот же результат без побочных эффектов (взаимодействие с чем-то вне нашего приложения, которое может изменить наше состояние, например API) попутно. Это идеально подходит для того, чтобы делать то, что нам нужно, чтобы иметь надежные значения, например, для управления состоянием.

Действия

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

Что за действие? Это простой объект JavaScript, который сначала сообщает тип действия, которое хочет выполнить пользователь.

Если пользователь хочет увеличить счетчик, действие выглядит так:

{ type: ‘INCREMENT’ }; // action to increment counter

Мы предоставили тип действия, которое мы хотим или пользователь хочет выполнить со свойством type. Типом является строка, и по соглашению он должен быть прописным, как константа, чтобы сделать его максимально понятным.

Теперь насчет действия декремента. Остановитесь на минутку и посмотрите, сможете ли вы сделать это самостоятельно:

{ type: ‘DECREMENT’ } // action to decrement counter

Теперь нам нужно добавить некоторую дополнительную логику в наш редуктор, чтобы обновлять состояние соответствующим образом в соответствии с типом.

Вы можете подумать, что было бы уместно использовать if / else, но обратите внимание, что у некоторого редуктора может быть много, много условий, что делает оператор switch лучшим и более кратким выбором.

Итак, давайте перепишем нашу функцию:

function counterReducer(count, action) {
  switch (action.type) {
    case "INCREMENT":
      return count + 1;
    case "DECREMENT":
      return count - 1;
    default:
      return count;
  }
}

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

А также, если мы по ошибке передадим этому редуктору действие, которое не соответствует ни одному из вариантов, он просто запустит вариант по умолчанию и вернет текущее состояние.

Так что давайте проверим это еще раз. Давайте увеличим, а затем уменьшим наш счетчик:

counterReducer(0, { type: ‘INCREMENT’ }) // 1

Итак, сначала у нас есть 1, затем возьмем эту единицу и уменьшим ее на единицу, и у нас должно получиться 0:

counterReducer(1, { type: ‘DECREMENT’ }) // 0

И мы делаем.

Неизменность редукторов

В наших приложениях у нас будут более сложные значения, чем просто одно число. Скорее всего, это никогда не будет примитив JS в состоянии, а будет объект, который мы используем для организации нашей информации. В этом есть смысл. На объекте мы можем упорядоченно организовать и управлять гораздо большим объемом данных.

Итак, давайте переосмыслим наш пример с объектом, и вместо того, чтобы использовать count как все состояние, у нас будет целый объект состояния с несколькими свойствами. Мы также знаем, что чистые функции должны быть неизменяемыми, так как же нам это сделать сейчас для значения состояния, являющегося объектом?

Сначала изменим count на state. И count теперь просто собственность на state:

function counterReducer(state, action) {
  switch (action.type) {
    case "INCREASE":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

Сейчас мы просто обновляем свойство count, но скажем, если бы у нас были другие свойства, мы бы также хотели объединить их в один объект с состоянием счетчика. Мы могли бы легко сделать это с помощью оператора распространения следующим образом:

function counterReducer(state, action) {
  switch (action.type) {
    case "INCREASE":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

Это важная часть использования редукторов для управления состоянием приложения. Состояние управляется в основном через объекты, и обновления состояния всегда должны быть неизменными. Мы создаем новый объект состояния из входящего состояния и части, которую хотим изменить (например, свойство count). Таким образом мы гарантируем, что другие свойства, которые не касаются входящего объекта состояния, остаются неизменными для нового объекта состояния. Таким образом, этот шаблон распространения в старом состоянии и обновления одного элемента состояния, который контролирует редуктор для создания нового объекта, станет очень знакомым шаблоном.

Давайте создадим новый редуктор, который будет контролировать имя и адрес электронной почты текущего пользователя. Поскольку он будет управлять состоянием пользователя, мы будем называть его редуктором пользователя и иметь в качестве параметров состояние и действие. Мы сделаем два случая: одно - изменить имя, другое - адрес электронной почты.

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
    case "CHANGE_EMAIL":
  }
}

Полезные нагрузки

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

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

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

{ type: 'CHANGE_NAME', payload: { name: 'Joe' } }

Затем обратно в наш переключатель, чтобы обновить состояние, мы можем вернуть и объект, в котором мы распространяем все другие свойства состояния, которые мы не обновляем до нового объекта состояния. А затем, чтобы получить полезную нагрузку для обновления имени, допустим, что initialState состоит из свойства name и email:

const initialState = {
  name: "Mark",
  email: "[email protected]",
};

Мы можем просто установить для свойства name значение action.payload.name. Это так просто. Поскольку это примитивное значение, а не ссылочное значение, нам не нужно беспокоиться о копировании здесь:

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
      return { ...state, name: action.payload.name };
    case "CHANGE_EMAIL":
  }
}

И мы можем сделать то же самое с электронной почтой. Давайте сначала напишем действие:

{ type: 'CHANGE_EMAIL', payload: { email: '[email protected]' } }

И затем условие, обязательно укажите в конце наш случай по умолчанию. Обратите внимание, что перед ним нет регистра ключевого слова, просто значение по умолчанию:

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
      return { ...state, name: action.payload.name };
    case "CHANGE_EMAIL":
      return { ...state, email: action.payload.email };
    default:
      return state;
  }
}

Теперь давайте выполним эти обновления состояния, передав начальное состояние:

const initialState = {
  name: "Mark",
  email: "[email protected]",
};

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
      return { ...state, name: action.payload.name };
    case "CHANGE_EMAIL":
      return { ...state, email: action.payload.email };
    default:
      return state;
  }
}

const action = {
  type: "CHANGE_EMAIL",
  payload: { email: "[email protected]" },
};

userReducer(initialState, action); // {name: "Mark", email: "[email protected]"}

Резюме

Вы станете более уверенными в редукторах, если будете использовать их в своих собственных приложениях. Они должны быть концепцией, которая упрощает наш код, помогая нам делать обновления состояния более предсказуемыми.

Вот основные вещи, которые вы должны знать о редукторе в будущем:

  • Синтаксис: По сути, функция редуктора выражается как (состояние, действие) = ›newState.
  • Неизменяемость: состояние никогда не меняется напрямую. Вместо этого редуктор всегда создает новое состояние.
  • Переходы состояний: редуктор может иметь условные переходы состояний.
  • Действие: Обычный объект действия поставляется с обязательным свойством типа и необязательной полезной нагрузкой: свойство типа выбирает условный переход состояния. Полезные данные действия предоставляют информацию о переходе между состояниями.

Хотите стать мастером JS? Присоединяйтесь к 2020 JS Bootcamp 🏕️

Follow + Say Hi! 🎨 TwitterInstagramreedbarger.comcodeartistry.io