API хуков в React

58

1/4/2022

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

Вадим Пашаев

Вадим Пашаев

CEO PXSTUDIO_

API хуков в React

В этом справочном руководстве мы обсудим все изначально доступные хуки в React, но сначала давайте начнем с основных хуков React: useState, useEffect и useContext.

Для справки

Базовые хуки React
Базовые хуки React

useState

Сигнатура для хука useState выглядит следующим образом:

const [state, setState] = useState(initialState)

Здесь state - значение состояния, setState - функция обновления этого состояния. Оба они возвращаются после вызова функции useState с некоторым начальным состоянием initialState.

Важно отметить, что при первоначальном рендеринге вашего компонента начальное состояние будет соотвествовать initialState.

Для справки

Начальное состояние
Начальное состояние

Кроме того, для обновления состояния необходимо вызвать функцию обновления состояния setState с новым значением состояния, как показано ниже:

setState(newValue)

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

Очередь рендеринга React
Очередь рендеринга React

Обратите внимание, что функция setState асинхронная

Для справки

Ссылка на функцию setState никогда не изменяется во время повторной визуализации.

Почему это важно? Совершенно нормально иметь функцию обновления в списке зависимостей других хуков, таких как useEffect и useCallback, как показано ниже:

useEffect(() => {
  setState(5)
}, [setState]) //setState никогда не изменится, поэтому useEffect вызывается только в момент монтирования

Обратите внимание, что если функция обновления возвращает то же значение, что и текущее состояние, последующий повторный рендеринг пропускается:

Пропуск повторного рендеринга
Пропуск повторного рендеринга

Функциональное обновление

Функция обновления состояния, возвращаемая useState, может быть вызвана двумя способами. Первый - передать новое значение напрямую в качестве аргумента:

const [state, setState] = useState(initialStateValue)

// обновление состояния
setState(newStateValue)

Это правильно и отлично работает в большинстве случаев. Однако есть случаи, когда предпочтительна другая форма обновления состояния: функциональные обновления.

Вот пример выше, измененный для использования функциональной формы обновления:

const [state, setState] = useState(initialStateValue)

// обновление с помощью функции
setState((previousStateValue) => newValue)

Вы передаете аргумент функции в setState. Внутри React вызовет эту функцию с предыдущим состоянием в качестве аргумента. Все, что возвращается из этой функции, устанавливается как новое состояние.

Давайте рассмотрим случаи, когда этот подход предпочтительнее.

1. Новое значение состояния зависит от предыдущего состояния.

Когда ваше новое состояние зависит от предыдущего значения состояния, например, вычисление нового значения счетчика, отдайте предпочтение обновлению состояния с помощью функции. Поскольку setState является асинхронным, React гарантирует, что предыдущее значение состояния является актуальным.

Например:

function GrowingButton() {
  const [width, setWidth] = useState(50)

  // вызывается функция setWidth с передачей в нее функции
  const increaseWidth = () => setWidth((previousWidth) => previousWidth + 10)

  return (
    <button style={{ width }} onClick={increaseWidth}>
      I grow
    </button>
  )
}

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

import { useState } from "react"

export default function GrowingButton() {
  const [width, setWidth] = useState(50)

  // вызываем setWidth с функцией обновления
  const increaseWidth = () => setWidth(previousWidth => previousWidth + 10)

  return (
    <>
      <h1>Клик по кнопке увеличит ширину кнопки:</h1>
      <button style={{ width }} onClick={increaseWidth}>
        Клик
      </button>
    </>
  )
}

2. Слияние объекта состояния

Давайте взглянем на следующий кусок кода:

import { useState } from 'react'

export default function Home() {
  const [state, setState] = useState({ name: 'React' })
  const updateState = () => setState({ creator: 'Facebook' })

  return (
    <div>
      <pre>{JSON.stringify(state)}</pre>
      <button onClick={updateState}>Обновить состояние</button>
    </div>
  )
}

Когда вы нажмете кнопку обновления состояния, какое из значений состояния выведется ниже?

//1. 
{"name": "React", "creator": "Facebook"}

//2. 
{"creator": "Facebook"}

//3. 
{"name": "React"}

Правильный ответ - 2, потому что с хуками функция обновления не объединяет объекты, в отличие от функции setState в классовых компонентах. Он заменяет значение состояния любым новым значением, переданным в качестве аргумента.

Но это можно исправить с помощью функциональной формы функции обновления состояния:

const updateState = () =>  setState(prevState => ({ ...prevState, creator: "Facebook" }))

Передайте функцию в setState и верните объединенный объект с помощью оператора spread-оператора (Object.assign также работает).

3. Избегайте зависимости от состояния в других хуках

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

Взглянем на пример ниже:

const [state, setState] = useState(0) 

// до
useEffect(() => {
  setState(state * 10)
}, [state, setState]) // добавим зависимости для того, чтобы пройти проверки eslint

// после: если ваша цель вызвать функцию только при монтировании
useEffect(() => {
  setState(prevState => prevState * 10)
}, [setState]) //Удаляем зависимость от state и теперь функция setState может безопасно использоваться

Ленивая инициализация состояния

Аргумент initialState для useState используется только во время первоначального рендеринга.

// правильно
const [state, setState] = useState(10) 

// последующие обновления пропса игнорируются
const App = ({ myProp }) => {
  const [state, setState] = useState(myProp)
}
// только начальное значение myProp при первом рендеринге передается в качестве initialState. Последующие обновления игнорируются.

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

const [state, setState] = useState(() => yourExpensiveComputation(props))

Спасение от обновления состояния

Если вы попытаетесь обновить состояние с тем же значением, что и текущее состояние, React не будет отображать дочерние компоненты или активировать эффекты, например, обратные вызовы useEffect. React сравнивает предыдущее и текущее состояние с помощью алгоритма сравнения Object.is; если они равны, повторный рендеринг игнорируется.

Важно отметить, что в некоторых случаях React может по-прежнему отображать конкретный компонент, состояние которого было обновлено. Это нормально, потому что React не будет углубляться в дерево, т. е. рендерить дочерние элементы компонента.

Для справки

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

useEffect

Базовая сигнатура useEffect выглядит следующим образом:

useEffect(() => {

})

Первым параметров в useEffect передается функция (коллбэк), которая в идеале содержит некоторый императивный код. Например, использующий мутации, подписки, таймеры, регистраторы и т. д. — по сути, побочные эффекты, которые не разрешены внутри основного тела вашего функционального компонента.

Основная часть функции относится к блоку перед оператором возврата функции.

Наличие таких побочных эффектов в основном теле вашей функции может привести к запутанным ошибкам и несовместимым пользовательским интерфейсам. Не делайте этого. Используйте useEffect.

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

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

useEffect(() => {
  // этот коллбэк будет вызываться после каждого рендера
})

Очистка эффекта

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

useEffect(() => {
  const subscription = props.apiSubscription() 

  return () => {
     // отписка от подписки :)
     subscription.unsubscribeApi()
   }
})

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

А что насчет случаев, когда компонент рендерится несколько раз, например, определенный компонент А рендерится дважды? В этом случае при первом рендеринге подписка на эффект настраивается и очищается перед вторым рендерингом. Во втором рендере настраивается новая подписка.

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

Время эффекта

Существует очень большая разница между вызовом коллбэка в useEffect и вызовом таких методов класса, как componentDidMount и componentDidUpdate.

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

Так обстоит дело с useEffect, но такое поведение не всегда идеально.

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

Для таких случаев React предоставляет еще один хук под названием useLayoutEffect. Он имеет ту же сигнатуру, что и useEffect; единственная разница заключается в том, когда он запускается, т. е. когда вызывается коллбэк-функция.

Заметьте, несмотря на то, что useEffect не вызывается до тех пор, пока браузер не отрисует макет, он по-прежнему гарантированно сработает до любого повторного рендеринга. Это важно!

Для справки

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

Условное срабатывание эффекта

По-умолчанию, коллбэк в useEffect вызывается при каждом рендере компонента.

useEffect(() => {
  // Вызывается при каждом рендере
})

Это сделано для того, чтобы эффект пересоздавался при изменении любой из его зависимостей. Это здорово, но иногда это излишне.

Рассмотрим пример, который у нас был в предыдущем разделе:

useEffect(() => {
  const subscription = props.apiSubscription()

  return () => {
    // Очистка подписки
    subscription.unsubscribeApi()
  }
})

В этом случае нет особого смысла пересоздавать подписку каждый раз, когда происходит рендеринг. Это следует делать только при изменении props.apiSubscription.

Для обработки таких случаев useEffect принимает второй аргумент, известный как массив зависимостей.

useEffect(() => {

}, [])

В приведенном выше примере мы можем предотвратить выполнение вызова эффекта при каждом рендеринге следующим образом:

useEffect(() => {
   const subscription = props.apiSubscription() 

  return () => {
     // очищаем подписку
     subscription.unsubscribeApi()
   }
}, [props.apiSubscription]) // взгляните сюда

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

Если вы хотите, чтобы ваш эффект запускался только при монтировании (очищался при размонтировании), передайте пустую зависимость массива:

useEffect(() => {
   // коллбэк эффекта будет запускаться при монтировании
   // и очищаться при размонтировании
}, [])

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

useEfect(() => {
  console.log(props1 + props2 + props3)
}, [props1, props2, props3])

Допустим, вы сделали это:

useEffect(() => {
  console.log(props1 + props2 + props3)
},[])

props1, props2 и props3 будут иметь только свои начальные значения, и коллбэк эффекта не будет вызываться при их изменении.

Если вы пропустили один из них, например, props3:

useEfect(() => {
  console.log(props1 + props2 + props3)
}, [props1, props2])

Тогда обратный вызов эффекта не будет выполняться при изменении props3.

Ребята из команды React рекомендуют использовать пакет eslint-plugin-react-hooks. Он предупреждает, когда зависимости указаны неправильно, и предлагает исправление.

Вы также должны отметить, что вызов коллбэка useEffect будет запущен по крайней мере один раз. Вот пример:

useEfect(() => {
  console.log(props1)
}, [props1])

Предполагая, что props1 обновляется один раз, т.е. изменяется со своего начального значения на другое, сколько раз вы бы зарегистрировали props1?

  1. Один раз, когда компонент монтируется;
  2. Один раз, при изменении props1;
  3. Дважды: при монтировании и изменении props1.

Правильный ответ — 3, потому что вызов коллбэка эффекта сначала запускается после первоначального рендеринга, а последующие вызовы происходят при изменении props1. Помните об этом.

Наконец, массив зависимостей не передается в качестве аргументов функции эффекта. Хотя это действительно похоже на это; это то, что представляет массив зависимостей. В будущем у команды React может появиться продвинутый компилятор, который автоматически создаст этот массив. А пока не забудьте добавить их самостоятельно.

Подписаться на рассылку

Получите лучшие новости по веб-разработке и AI

Подписаться на рассылку

Получите лучшие новости по веб-разработке и AI

Оценка проекта

Хотите быструю оценку Вашего проекта?

Василий Иванов
Максим Насенников
Виктория Мальцева
Vadim Pashaev

Заполните форму справа и наша команда экспертов поможет найти для Вас оптимальное решение вашей идеи или задачи

Есть интересная идея?

И вы очень хотите ее реализовать, пишите нам и получите подробное коммерческое предложение и быструю реализацию