Почему в React важен порядок вызова хуков?

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

Непростая ситуация

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

const { data, loading } = useGetData();
const userId = data.id;
const { relatedData } = useGetRelatedDataForUser(userId);

Проблема здесь заключается в том, что userId = data.id может быть еще не определено. Прежде чем выполнять второй вызов, вы, возможно, захотите проверить, является ли загрузка loading false:

const { data, loading } = useGetData();
if (!loading){
const userId = data.id;
const { relatedData } = useGetRelatedDataForUser(userId);
}

Однако если сделать что-то подобное, в консоли появится ошибка, связанная с нарушением правил использования хуков. Правила использования хуков таковы.

  1. Вызывать хуки можно только на верхнем уровне компонента функции.
  2. Вызывать хуки можно только на верхнем уровне другого хука.

Заметим, что хук  —  это любая функция, имя которой начинается со строки use. Это означает, что:

  1. нельзя вызывать хук внутри условия или цикла;
  2. нельзя вызывать хук после условного возврата;
  3. нельзя вызывать хук в компоненте класса.

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

Почему порядок вызовов хуков имеет значение

React полагается на порядок вызовов хуков для синхронизации с состоянием и жизненным циклом компонента. Каждый вызов хука связан с определенным элементом состояния или жизненного цикла, и React поддерживает порядок выполнения этих хуков.

Каждый вызов хука имеет отношение к узлу в связанном списке, который React отслеживает для каждого компонента. В этом узле хранится значение состояния и указатель на следующий узел (или хук).

Рассмотрим для примера React-компонент с двумя переменными состояния: ‘name’ и ‘age’.

const [name, setName] = useState('Harris');
const [age, setAge] = useState(15);dnf install gcc-gfortran python3-devel python2-devel openblas-devel lapack-devel Cython
return (
<div classname="App">
<h1>{name}</h1>
<h1>{age}</h1>
<button onClick={() => {setAge(age+1)}}>Add a Year!</button>
<button onClick={() => {setName('Buddy')}}>Change Name</button>
</div>
)

При первом рендере React создает связный список с двумя узлами для ‘name’ и ‘age’. Эти узлы не содержат никакой информации о том, какой переменной они принадлежат, а просто хранят значение переменной состояния.

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

При необходимости обновить переменную состояния, например ‘age’, функция-setter, возвращаемая useState, знает, какой узел в связанном списке нужно обновить. Происходит это потому, что при создании функции-setter она соотносится с определенным индексом в связанном списке.

React-хуки и условные вызовы

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

let died = false;
if (died) {
const [deathDate, setDeathDate] = useState(new Date());
}
const [name, setName] = useState('Harris');
const [age, setAge] = useState(15);return (
<div classname=”App”>
<h1>{name}</h1>
<h1>{age}</h1>
<button onClick={() => {
if (!died) {
setAge(age+1);
died = true;
}
}}>Add a Year!</button>
<button onClick={() => {setName(‘Buddy’)}}>Change Name</button>
</div>
)

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

Изучение альтернативных вариантов дизайна

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

При разработке React-хуков рассматривались различные альтернативные варианты дизайна. Рассмотрим некоторые из них и связанные с ними проблемы.

Одна из идей заключается в том, чтобы иметь единый объект состояния:

const [state, setState] = useState({
name: “Harris”,
age: 15
})

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

const [name, setName] = useState(‘name’);
const [age, setAge] = useState(‘age’);

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

В качестве еще одной альтернативы рассматривалось использование Symbol’ов:

const count = Symbol('count');
const MyComponent = () => {
const [count, setCount] = useState(count, 0);
}

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Harris Ferguson: Why does hook call order matter? The Rules of React Hooks explained

Предыдущая статьяПринцип открытости/закрытости: расширение кода без модификации
Следующая статьяГлубокое погружение в Java: рефлексия и загрузчик классов. Часть 1