Продолжение "3 ошибки, которые делают младшие разработчики с состоянием компонента React"

Несколько недель назад я написал статью об ошибках, которые иногда допускают разработчики при работе с состоянием компонентов React. Во всех приведенных мною примерах использовались компоненты класса и метод setState.

Меня несколько раз спрашивали, применимы ли эти же принципы к функциональным компонентам и хукам. Ответ положительный!

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

1. Изменение состояния напрямую

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

Давайте посмотрим на пример. Допустим, у вас есть состояние, которое выглядит так:

const initialState = ['red', 'blue', 'green']
let [colors] = useState(initialState)

И теперь вы хотите добавить к этому массиву цвет yellow. Может возникнуть соблазн сделать это:

colors.push('yellow')

Или даже это:

colors = [...colors, 'yellow']

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

Вот как правильно добавить элемент в массив:

// Initial setup
const initialState = ['red', 'blue', 'green']
const [colors, setColors] = useState(initialState)
// Later, modifying the state
setColors(colors => [...colors, 'yellow'])

И это приводит нас прямо к ошибке №2.

2. Установка состояния, основанного на предыдущем состоянии, без использования функции

Есть два способа использовать метод установки, возвращаемый ловушкой useState. Первый способ - предоставить новое значение в качестве аргумента. Второй способ - предоставить функцию в качестве аргумента. Итак, когда вы хотите использовать одно вместо другого?

Если бы у вас была, например, кнопка, которую можно включать или отключать, у вас мог бы быть кусок состояния с именем isDisabled, который содержит логическое значение. Если вы хотите переключить кнопку с включенного на отключенное, может возникнуть соблазн написать что-то вроде этого, используя значение в качестве аргумента:

// Initial setup
const [isDisabled, setIsDisabled] = useState(false)
// Later, modifying the state
setIsDisabled(!isDisabled)

Итак, что в этом плохого? Проблема заключается в том, что обновления состояния React могут быть пакетными, что означает, что несколько обновлений состояния могут происходить в одном цикле обновления. Если бы ваши обновления были пакетными и у вас было несколько обновлений для включенного / отключенного состояния, конечный результат может быть не таким, как вы ожидаете.

Лучшим способом обновить состояние здесь было бы предоставить функцию предыдущего состояния в качестве аргумента:

// Initial setup
const [isDisabled, setIsDisabled] = useState(false)
// Later, modifying the state
setIsDisabled(isDisabled => !isDisabled)

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

То же самое верно и для чего-то вроде увеличения счетчика.

Не делайте этого:

// Initial setup
const [counterValue, setCounterValue] = useState(0)
// Later, modifying the state
setCounterValue(counterValue + 1)

Сделай это:

// Initial setup
const [counterValue, setCounterValue] = useState(0)
// Later, modifying the state
setCounterValue(counterValue => counterValue + 1)

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

3. Забыть, что метод установки из useState является асинхронным.

Наконец, важно помнить, что метод установки, возвращаемый ловушкой useState, является асинхронным. В качестве примера представим, что у нас есть компонент со следующим состоянием:

const [name, setName] = useState('John')

А затем у нас есть метод, который обновляет состояние и записывает его в консоль:

const setNameToMatt = () => {
  setName('Matt')
  console.log(`The name is now... ${name}!`)
}

Вы можете подумать, что это приведет 'Matt' к консоли, но это не так! Логи 'John'!

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

Если у вас есть код, который нужно запустить после обновления состояния, React предоставляет ловушку useEffect, которая позволяет вам писать код, который запускается после обновления любой из указанных зависимостей.

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

Правильный способ регистрации текущего состояния после обновления:

useEffect(() => {
  if (name !== 'John') {
    console.log(`The name is now... ${name}!`)
  }
}, [name])

const setNameToMatt = () => setName('Matt')

Намного лучше! Теперь он правильно регистрирует 'Matt', как и ожидалось.

Примечание. В данном случае я добавил оператор if, чтобы предотвратить появление журнала консоли при первом монтировании компонента. Если вам нужно более общее решение, рекомендуется использовать ловушку useRef для хранения значения, которое обновляется после монтирования компонента. Это предотвратит запуск ваших useEffect обработчиков при первом монтировании компонента.

Заключение

Вот и все! Три распространенные ошибки и как их исправить. Вы можете найти код на GitHub.

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