Что вы можете не знать о хуке useState в React

Что вы можете не знать о хуке useState в React

13 февраля 2022 г.

В React есть два типа компонентов — функциональные и классовые. Традиционно только компоненты класса могли иметь методы состояния и жизненного цикла. Однако все изменилось с появлением хуков в React 16.8.


Теперь функциональные компоненты также имеют доступ к методам состояния и жизненного цикла. Для простых состояний мы используем хук useState, а для обработки более сложных состояний мы используем хук useReducer. Для методов жизненного цикла мы используем хук useEffect.


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


Что такое хуки?


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


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


Это не означает, что поведение или реализация хуков такие же, как у setState или методов жизненного цикла в компонентах класса. Это далеко не так, как вы скоро увидите на примере useState.


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


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


Хук useState


Сегодня мы поговорим конкретно о хуке useState. Хук useState позволяет нам иметь простое состояние в функциональных компонентах.


Это именованный экспорт из библиотеки React, что означает, что мы можем импортировать его следующим образом:


```js


импортировать React, {useState} из "реагировать"


Или вы можете просто использовать оператор .:


```js


импортировать React из «реагировать»


React.useState() // также работает


Хук useState принимает 1 аргумент, который является начальным состоянием, и возвращает массив. Массив содержит 2 элемента: переменную состояния и функцию установки.


Итак, если мы взглянем на базовую структуру хука useState, она будет примерно такой:


```js


функция экспорта useState (initialState) {


// логика


вернуть [состояние, setState]


При начальном рендеринге state будет таким же, как initialState. Однако initialState игнорируется после первого рендеринга. Следовательно, переменная состояния всегда будет возвращать текущее состояние.


Если начального состояния нет, то переменная состояния вернет «undefined» при первом рендеринге.


Функция установки (setState) используется для изменения значения состояния.


Базовое использование


Во-первых, мы импортируем useSate из React.


```js


импортировать React, {useState} из "реагировать"


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


```js


функция Счетчик() {


const [количество, setCount] = useState (0)


вернуть

{количество


// отображает 0 в пользовательском интерфейсе


PS: На этом этапе мы также можем лениво инициализировать начальное состояние. Мы поговорим об этом позже.


Также можно использовать индекс массива.


```js


постоянный счетчик = useState (0) [0]


константа setCount = useState (0) [1]


Хотя это работает нормально, это явно неудобно.


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


```js


// если useState вернул объект


const {состояние: количество, setState: setCount} = useState (0)


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


Обновление состояния


Чтобы обновить состояние, мы используем функцию установки, которая возвращается хуком useState. В приведенном выше примере это функция setCount.


```js


установитьКоличество(1)


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


Обычно мы будем использовать функцию установки внутри обработчика кликов, хука useEffect или непосредственно внутри атрибута onClick.


```js


возвращение (


{количество



// увеличивается на 1 при каждом клике


Теперь, когда мы нажимаем кнопку, переменная count увеличивается на 1. Однако важно отметить, что функции установки на самом деле асинхронны. Поэтому, когда мы нажимаем кнопку, мы просто добавляем обновление в очередь. Он выполняется не сразу. Это связано с волоконной архитектурой реакции.


Прямо сейчас мы рассмотрели самое простое использование хука useState. У нас есть,


  • Импортировал это

  • Пройдено начальное состояние

  • Деструктурировано состояние и функция установки

  • Использована функция установки для обновления состояния

PS: Мы пока только передали число как состояние. Мы действительно можем пройти что угодно. В ближайшее время мы рассмотрим каждый конкретный случай. А пока давайте взглянем на два расширенных шаблона useState. Понимание их дополнит остальную часть содержания статьи.


Расширенные шаблоны useState


1. Ленивая инициализация


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


Вы можете подумать, зачем это вообще нужно. Хорошо, если значение состояния является результатом какого-то сильного вычисления, например, проанализированного JSON, поступающего из вызова API. В этом случае вы не хотите, чтобы пользовательский интерфейс зависал до тех пор, пока это вычисление не будет завершено и состояние не будет инициализировано. Излишне говорить, что это будет плохой пользовательский опыт.


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


```js


const [количество, setCount] = useState (() => 0)


В приведенном выше случае функция — это та функция, которая передается в качестве начального состояния хуку useState. Хук useState определен таким образом, что если функция передана, она будет выполняться и присваивать возвращаемое значение переменной состояния. Чего он не будет делать, так это поддерживать процесс useState до тех пор, пока вычисления не будут завершены. Таким образом, он гарантирует, что пользовательский интерфейс отображается независимо от того, было ли инициализировано состояние.


Вы также можете определить функцию снаружи и просто передать ее в useState вот так.


```js


вычисление функции () {


// сильное вычисление


const [состояние, setState] = useState(() => вычисление())


Здесь функция computation() запускается по мере необходимости и не блокирует рендеринг пользовательского интерфейса.


2. Использование предыдущего состояния для обновления текущего состояния


Вернемся к приведенному выше примеру.


```js


const React, {useState} из 'реагировать';


функция Счетчик() {


const [количество, setCount] = useState (0)


возвращение (


{число



Здесь мы напрямую передаем переменную состояния и добавляем 1 внутрь setCount(). Проблема с этим подходом заключается в том, что мы не можем сделать второе обновление с обновленным значением. Например,


```js


функция Счетчик() {


const [количество, setCount] = useState (0)


функция handleClick() {


setCount(count + 1) // возвращает 0 + 1 = 1


setCount(count + 2) // возвращает 0 + 2 = 2


возвращение (


{число



// конечное значение состояния будет 2, а не 3


В приведенном выше случае вы могли бы подумать, что сначала count будет 1, затем он прибавит 2 и станет 3. Тогда вы думаете неправильно.


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


  1. Этап рендеринга

  1. Фаза фиксации

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


В приведенном выше случае оба setCount были инициализированы 0 и добавлены в очередь. Поскольку JavaScript принимает последнее значение как окончательное, значение состояния равно 2.


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


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


```js


функция handleClick() {


setCount(prevCount => prevCount + 1) // возвращает 0 + 1 = 1


setCount(prevCount => prevCount + 2) // возвращает 1 + 2 = 3


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


Таким образом, и хук useState(), и функция установки имеют специальную проверку, чтобы увидеть, является ли переданное значение функцией. В остальном они работают так же, как и два приведенных выше шаблона.


Использование useState с примитивными типами данных


Мы уже использовали useState с примитивным типом данных — числом. Но на самом деле вы также можете использовать другие примитивные типы данных, такие как логические или строковые.


```js


const [isLoggedIn, setIsLoggedIn] = useState(false) // с использованием логического значения


const [name, setName] = useState("Илон Маск") // использование строки


Важно понимать, что когда мы изменяем значение isLoggedIn или name, мы фактически создаем новую переменную с этим значением. Именно так работает JavaScript. Это не относится к примитивным типам данных, но, как вы скоро увидите, будет иметь значение для массивов и объектов.


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


```js


isLoggedIn = Истина


console.log(isLoggedIn) // печатает «true» в консоли


// однако рендеринга не будет


// потому что реакция не знает, что переменная состояния изменилась


// правильный способ сделать это


setIsLoggedIn (истина)


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


Использование useState с массивами


Мы также можем использовать массивы в качестве переменной состояния.


```js


const [список, setList] = useState(["Яблоко", "Апельсин", "Виноград"])


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


```js


setList([... список, "Банан"])


Здесь оператор распространения ... будет клонировать содержимое существующего массива, и, наконец, Banana будет добавлен в конец.


Или вы можете добавить Банан вверху, поменяв позицию.


```js


setList(["Банан", ... список])


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


```js


const newList = list.filter(item => item !== "Grape") // или какая-то другая логика


установитьСписок([...новыйСписок])


// список становится ["Яблоко", "Апельсин", "Банан"]


// (Виноград отфильтровывается)


Вы можете использовать pop(), splice(), shift() и другие методы для удаления элементов из массива. Здесь важны ваши обычные навыки работы с JavaScript. Например, вам, возможно, придется иметь дело с объектами внутри массивов и т.д.


Работа с многомерными массивами


Многомерные массивы — это массивы, внутри которых есть другие массивы. Оператор распространения углубляется только на один уровень при копировании контента. Таким образом, он не будет вести себя должным образом с многомерными массивами. Если вы обнаружите, что имеете дело с многомерными массивами, вам лучше попытаться переделать логику для обработки более простых объектов состояния.


Вы по-прежнему можете использовать методы массива, такие как map(), а затем создавать сложную логику для работы с ними. Это просто не идеально.


Использование useState с объектами


Мы также можем передавать объекты как состояние в хуке useState.


```js


const [пользователь, setUser] = useState({


Имя: "Илон",


фамилия: "Маск",


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


```js


setUser({...пользователь, возраст: "50" })


Обратите внимание на разницу в синтаксисе. Для массивов мы используем скобки [], а для объектов мы используем {}. Итак, мы создаем новый массив/объект.


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


```js


setUser({...user, lastName: "Сумерки" })


// пользователь становится {


// имя: "Элон",


// фамилия: "Сумерки",


Работа с вложенными объектами


Та же проблема сохраняется и с оператором спреда. Оператор спреда углубляется только на один уровень. Так что если у нас есть вложенные объекты, они не будут клонированы оператором распространения. Представьте ситуацию:


```js


постоянный человек = {


Имя: "Илон",


фамилия: "Маск",


дочерний случайный: {


Имя: "Гриффин",


фамилия: "Маск",


const [пользователь, setUser] = useState (человек)


Предположим, нам нужно добавить возраст Гриффина Маска. Как бы вы это сделали? Я представляю, что это будет так:


```js


setUser({...user, childRandom.age = 18}) // неправильно


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


```js


setUser({...пользователь, childRandom: {


...user.childRandom, возраст: 18


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


```js


постоянный отец = {


Имя: "Илон",


фамилия: "Маск",


постоянный ребенок = {


Имя: "Гриффин",


фамилия: "Маск",


const [userOne, setUserOne] = useState (отец)


const [userTwo, setUserTwo] = useState (дочерний)


Когда использовать объекты или несколько вызовов useState, решать вам. Но слишком углубляться во вложенные объекты — всегда плохая идея.


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


Использование Props в качестве начального состояния


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


```js


функция пользователя (реквизит) {


const [имя, setName] = useState (props.name)


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


В этом случае вы можете использовать хук useEffect с этим конкретным свойством в качестве зависимости и вместо этого вызвать функцию установки.


```js


функция пользователя (реквизит) {


const [имя, setName] = useState (props.name)


использоватьЭффект(() => {


setName (реквизит.имя)


}, [реквизит.имя])


Если вы не знаете хук useEffect, он используется для имитации методов жизненного цикла. Требуется 2 аргумента,


  1. Функция

  1. Массив зависимостей

Тело функции содержит то, что нам нужно выполнить, а массив зависимостей определяет, какие изменения запускают useEffect.


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


Архитектура оптоволокна и процесс обновления


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


Итак, волоконная архитектура состоит из двух фаз:


  1. Этап рендеринга

  1. Фаза фиксации

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


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


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


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


Однако на этапе рендеринга все работает асинхронно. Таким образом, явно много прерываний в зависимости от приоритета.


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


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


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


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


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


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


Этап рендеринга и фиксации работает следующим образом:


Этап рендеринга


Если присутствует useState или useReducer, значение состояния инициализируется.


JSX компонента преобразуется в реагирующие элементы. Это делается с помощью метода React.createElement().


Это, в свою очередь, создает дерево компонентов. Это просто объектное представление фактического компонента. Мы называем это виртуальным DOM.


Фаза фиксации


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


Это все нормально, если наш компонент рендерится только один раз.


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


В этом случае компонент помечается, когда мы запускаем функцию установки. (Или отправить для useReducer).


PS: *Повторный рендеринг также может произойти, если есть изменение реквизита или из-за повторного рендеринга родителя. Мы говорим только о повторных рендерах, которые происходят из-за функций установки в * useState.


Во время повторного рендеринга фаза рендеринга и фаза фиксации действуют немного по-разному.


На этапе рендеринга переменная состояния проверяется на предмет изменения значения.


Если есть, снова вызывается React.createElement(), и JSX преобразуется в дерево компонентов. На этот раз вновь созданное дерево сравнивается с текущим деревом, чтобы увидеть, есть ли какие-либо изменения. Изменения применяются к виртуальной модели DOM и передаются на фазу фиксации.


Если переменная состояния не изменилась, тогда react полностью исключается из процесса рендеринга.


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


Защитная сетка


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


Это просто функция безопасности, используемая реакцией.


Правила хуков React


Хорошо, теперь давайте обсудим что-то гораздо более простое. У хуков React есть определенные правила (ну, 2), которым нужно следовать. Правила просты,


  1. Хук должен вызываться только на верхнем уровне компонента.

  1. Хук должен вызываться только для функциональных компонентов.

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


Однако для первого хук должен вызываться так:


```js


приложение функции () {


const [state, setState] = useState(0) // или любой другой хук


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


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


Хук useReducer и обработка сложных состояний


Хорошо, я знаю, что это уже длинная статья. Но это не будет полным, если мы кратко не коснемся хука useReducer.


Хук useReducer также помогает нам управлять состоянием в реакции. Фактически, useState является абстракцией от хука useReducer. За кулисами мы все еще используем useReducer с некоторыми фиксированными условиями, которые заставляют его вести себя как useState.


Хук useReducer принимает начальное состояние и функцию редуктора и возвращает массив. Массив имеет переменную состояния и метод отправки.


```js


импортировать React, {useReducer} из "реагировать"


приложение функции () {


const [состояние, отправка] = useReducer (редуктор, initialState)


Функция редуктора — это просто функция, которая содержит набор условий и возвращает что-то в зависимости от этого условия. Лучший способ добиться этого — использовать оператор switch, но никто не запрещает использовать вместо него что-то вроде if-else.


Простая реализация useReducer с примером счетчика:


```js


константное начальное состояние = { количество: 0 }


редуктор функции (состояние, действие) {


переключатель (действие.тип) {


случай "приращение":


вернуть {количество: state.count + 1}


случай "уменьшение":


вернуть {количество: state.count - 1}


дефолт:


выдать новую ошибку()


функция Счетчик() {


const [состояние, отправка] = useReducer (редуктор, initialState)


возвращение (


Количество: {state.count}




Идея здесь проста. Всякий раз, когда запускается функция dispatch, она отправляет свой аргумент функции редуктора. Функция редуктора сопоставляет это с предопределенными условиями и возвращает новое состояние. Таким образом, можно управлять более сложными состояниями. Возможно, вы могли бы эмулировать это, используя предыдущий шаблон состояния. Конечно, это будет сложнее.


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


Впервые опубликовано здесь



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE