Альтернатива доброте this.setState ()

Итак, с момента выпуска React hooks прошло довольно много времени. и, судя по всему, все сходят с ума от них. Ну я понял. потому что я тоже один из вас. Меня зацепили крючки!

Хуки позволяют нам создавать более мелкие, компонуемые, многоразовые и более управляемые компоненты React.

Иногда вы можете использовать хуки для управления состоянием формы, используя useState или useReducer.

Теперь давайте рассмотрим сценарий, в котором вам нужно управлять сложным состоянием формы с несколькими входами формы, которые могут быть нескольких разных типов, например текст, число, дата. Состояние формы может даже содержать вложенную информацию, например информацию об адресе пользователя, которая имеет собственные подполя, такие как address.addressLine1, address.addressLine2 и т. Д.

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

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

const [modalActive, updateModal] = useState(false)
.
.
.
// new state based on previous
updateModal(prev => !prev)

но, если у вас слишком много отдельных полей формы, например 100+ (ДА !!. Я управлял более чем 100 полями формы), то этот подход неудобен.

представлять себе !!..

const [firstName, setFirstName] = useState('')
const [middleName, setMiddleName] = useState('')
const [lastName, setLastName] = useState('')
.
.
.
.

Непрактично писать разные useState, а затем использовать отдельную функцию обновления для каждого поля.

Итак, другим вариантом будет перехватчик, useReducer.

Давайте посмотрим на пример.

const initialState = {
  firstName: '',
  lastName: ''
};
function reducer(state, action) {
  switch (action.type) {
    case 'firstName':
      return { firstName: action.payload };
    case 'lastName':
      return { lastName: action.payload };
    default:
      throw new Error();
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
return (
    <>
      <input
        type="text"
        name="firstName"
        placeholder="First Name"
        onChange={(event) => {
          dispatch({
           type: 'firstName',
           payload: event.target.value
          })
        }}
        value={state.firstName} />
      <input
        type="text"
        name="lastName"
        placeholder="Last Name"
        onChange={(event) => {
          dispatch({
           type: 'lastName',
           payload: event.target.value
          })
        }}
        value={state.lastName} />
   </>
  );
}

Эх !!, нехорошо.

Вы не можете записать каждый вариант использования для тех n полей формы в редукторе.

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

function reducer(state, action) {
  // field name and value are retrieved from event.target
  const { name, value } = action
  
  // merge the old and new state
  return { ...state, [name]: value }
}

Теперь этот редуктор выглядит лучше и чище.

Но… это не позволяет нам вычислять новое состояние на основе текущего состояния при вызове функции обновления с обратным вызовом. как мы можем сделать с ..

this.setState((prev) => ({ isActive: !prev }))
// or
const [modalActive, updateModal] = useState(false)
.
.
.
updateModal(prev => !prev)

также, как насчет обновления вложенного состояния, такого как address.addressLine1, address.pinCode.

Ok !!. у нас было много дискуссий об управлении сложным состоянием формы с использованием не очень идеальных подходов.

Позвольте мне просто показать вам решение.

Итак, вот полный исходный код для обработки таких сложных сценариев формы.

Я немного объясню функцию редуктора (EnhancedReducer: P).

Функция reducer получает два аргумента, первый аргумент - это текущее состояние до обновления. Этот аргумент предоставляется автоматически, когда вы вызываете функцию updateState / dispatch для обновления состояния редуктора. Второй аргумент функции reducer - это значение, с которым вы вызываете функцию updateState, это не обязательно должен быть типичный объект действия redux формы {type: 'something', payload : 'что-то' }. Это может быть что угодно: число, строка, объект или даже функция.

И это то, что мы используем. Если updateArg является функцией, мы вызываем ее с текущим состоянием, чтобы вычислить новое. Какой бы объект мы ни возвращали из этой функции, становится нашим новым состоянием.

И если updateArg - простой старый объект Javascript, то есть два случая.

1- Объект не имеет свойств _path и _value - и, следовательно, является обычным объектом обновления, точно так же, как мы передаем this.setState. Итак, вы можете просто вызвать updateState с новым объектом с частями состояния, которое вы хотите обновить, и он объединит его со старым и вернет новое состояние.

2- Объект имеет свойства _path и _value - когда функция updateState вызывается с объектом с этими двумя свойствами. мы рассматриваем это как особый случай, когда _path представляет собой путь к вложенному полю. В строковой форме, например: «address.pinCode» или в виде массива, представляющего путь [«адрес», «pinCode»].

Но что нам делать с такими представлениями пути, чтобы обновить вложенное поле в объекте? Мы используем метод set lodash. Он принимает обе формы пути как допустимые входные данные для обновления и объекта.

set(objectToUpdate, path, newValue)
const state = {
  name: {
   first: '',
   middle: '',
   last: ''
  }
}
// and to update, for eg: first name.
// both ways of path are correct.
set(state, 'name.first', 'Aditya')
set(state, ['name', 'first'], 'Aditya')

Но метод set изменяет объект на месте и не возвращает новую копию, но в мире React обнаружение изменений зависит от Неизменяемости, новой новой копии данных. , с новым местом в памяти.

Итак, чтобы обойти это, мы используем immer, который помогает обрабатывать неизменяемость с объектами Javascript в простой в использовании форме.

import produce from 'immer'
produce(state, draft => {
  set(draft, _path, _value);
});

Функция произвести из immer принимает объект для работы в качестве первого аргумента, который в нашем случае является текущим состоянием, а вторым аргументом является функция, которая получает черновик объекта для изменения, все, что вы изменяете внутри этой функции в черновом состоянии, выполняется на копии, а не на фактическом объекте ввода state на месте. а затем автоматически возвращает новый объект с обновленными данными.

Итак, наш улучшенный редуктор: D

Просто

yarn add lodash immer

и наслаждаться.

PS - Пример в сущности может быть уточнен намного дальше, с большим количеством крайних случаев, обрабатываемых в расширенномReducer, а код полей формы может быть сокращен путем сопоставления с объектом спецификации формы для его динамического создания и уменьшения дублирования кода. и еще кое-что.

Некоторые читатели могут иначе относиться к этому подходу. Так что мы всегда можем обсудить это в ветках.

Возможно, у некоторых из вас может возникнуть вопрос: если мы так пытаемся воспроизвести this.setState, то почему бы не использовать функцию обратного вызова второго аргумента setState, чтобы выполнить какое-то действие после обновления состояния. Что ж, это недостаточно декларативно! Мы будем использовать подход - сначала сделайте то, а потом сделайте то. Мы будем рассказывать коду, шаг за шагом, как что-то делать. Вместо того, чтобы просто сказать, что делать. Я бы использовал useEffect вместо функции обратного вызова и подхода «как делать», потому что это декларативный подход, реагирующий на изменения.

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

Ресурсы -