3.12.5 Правила использования хуков


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


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


Вызывайте хуки только на верхнем уровне


Не вызывайте хуки внутри циклов, условий или вложенных функций. Всегда используйте хуки только на самом верхнем уровне вашего компонента-функции React. Следуя этому правилу, вы гарантируете, что хуки вызываются в одном и том же порядке каждый раз при отрисовке компонента. Это именно то, что позволяет React правильно сохранять состояние хуков между несколькими вызовами useState и useEffect. (Если вам любопытно, мы подробно поговорим об этом ниже.)


Вызывайте хуки только из компонентов-функций React


Не вызывайте хуки из обычных функций JavaScript. Вместо этого вы можете:

  • ✅ Вызывать хуки из компонентов-функций React.

  • ✅ Вызывафть хуки из пользовательских хуков (с ними мы познакомимся в следующем разделе).

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




Мы выпустили ESLint плагин под названием eslint-plugin-react-hooks, который применяет два эти правила. Чтобы его попробовать, нужно добавить плагин в свой проект:


Код
    
  npm install eslint-plugin-react-hooks
  


Код
    
  // Ваша конфигурация ESLint
  {
    "plugins": [
      // ...
      "react-hooks"
    ],
    "rules": {
      // ...
      "react-hooks/rules-of-hooks": "error"
    }
  }
  

В будущем мы намерены добавить этот плагин в приложение «Create React App» (и другие подобные инструменты) по умолчанию.

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




Как мы узнали ранее, мы можем использовать несколько хуков состояния или эффекта в одном компоненте:


Код
    
  function Form() {
    // 1. Используем переменную состояния name
    const [name, setName] = useState('Вася');

    // 2. Используем эффект, чтобы сохранить данные формы
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });

    // 3. Используем переменную состояния surname
    const [surname, setSurname] = useState('Пупкин');

    // 4. Используем эффект, чтобы обновить название
    useEffect(function updateTitle() {
      document.title = name + ' ' + surname;
    });

    // ...
  }
  

Итак, каким образом React понимает, какое состояние какому вызову useState соответствует? Подход состоит в том, что React опирается на порядок, в котором вызываются хуки. Наш пример работает, потому что порядок вызовов хуков одинаков при каждой отрисовке:


Код
    
  // ------------
  // Первая отрисовка
  // ------------
  useState('Вася')           // 1. Инициализируем переменную состояния name значением 'Вася'
  useEffect(persistForm)     // 2. Добавляем эффект, чтобы сохранить данные формы
  useState('Пупкин')         // 3. Инициализируем переменную состояния surname значением 'Пупкин'
  useEffect(updateTitle)     // 4. Добавляем эффект, чтобы обновить название

  // -------------
  // Вторая отрисовка
  // -------------
  useState('Вася')           // 1. Считываем переменную состояния name (аргумент будет игнорирован)
  useEffect(persistForm)     // 2. Заменяем эффект, сохраняющий данные формы
  useState('Пупкин')        // 3. Считываем переменную состояния surname (аргумент будет игнорирован)
  useEffect(updateTitle)     // 4. Заменяем эффект, обновляющий название

  // ...
  

До тех пор, пока порядок вызова хуков одинаков между отрисовками, React с каждым из них может связать некоторое локальное состояние. Но что произойдет, если мы поместим вызов хука (например, эффект persistForm) в условие?


Код
    
  // 🔴 Нарушаем первое правило, используя хук в условии
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }
  

Во время первой отрисовки условие name !== '' равно true, поэтому мы запускаем этот хук. Однако при следующей отрисовке пользователь может очистить форму, сделав условие равным false. В таком случае мы пропускаем наш хук во время отрисовки, и порядок вызова хуков становится другим:


Код
    
  useState('Вася')           // 1. Считываем переменную состояния name (аргумент будет игнорирован)
  // useEffect(persistForm)  // 🔴 Вызов хука пропущен!
  useState('Пупкин')         // 🔴 2. (был 3-м). Неудача, при попытке считать переменную состояния surname
  useEffect(updateTitle)     // 🔴 3. (был 4-м). Неудача, при попытке заменить эффект
  

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

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


Код
    
  useEffect(function persistForm() {
      // 👍 Так мы больше не нарушаем первое правило
      if (name !== '') {
        localStorage.setItem('formData', name);
      }
  });
  

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




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