3.12.6 Пользовательский хук


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


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

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


Код
    
  import React, { useState, useEffect } from 'react';

  function FriendStatus(props) {
    const [isOnline, setIsOnline] = useState(null);

    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    useEffect(() => {
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
      };
    });

    if (isOnline === null) {
      return 'Загрузка...';
    }
    return isOnline ? 'Онлайн' : 'Офлайн';
  }
  

Теперь предположим, что в нашем приложении чата также есть список контактов, и мы хотим отображать имена онлайн-пользователей зеленым цветом. Мы могли бы скопировать и вставить приведенную выше логику в наш компонент FriendListItem, но такое решение неидеально:


Код
    
  import React, { useState, useEffect } from 'react';
  
  function FriendListItem(props) {
    const [isOnline, setIsOnline] = useState(null);
  
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
  
    useEffect(() => {
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
      };
    });
  
    return (
      <li style={{ color: isOnline ? 'green' : 'black' }}>
        {props.friend.name}
      </li>
      );
  }
  

Вместо этого мы бы хотели, чтобы компоненты FriendStatus и FriendListItem использовали эту логику совместно.

Традиционно в React у нас было два популярных способа для совместного использования логики состояния между компонентами: свойство render и HOC-и. Теперь мы рассмотрим, как хуки решают многие из тех же проблем, не заставляя вас добавлять в дерево дополнительные компоненты.




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



Пользовательский хук - это функция JavaScript, имя которой начинается с префикса use и которая может вызывать другие хуки. Например, useFriendStatus ниже - это наш первый пользовательский хук:


Код
    
  import React, { useState, useEffect } from 'react';

  function useFriendStatus(friendID) {
    const [isOnline, setIsOnline] = useState(null);

    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    useEffect(() => {
      ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
      };
    });

    return isOnline;
  }
  

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

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

Миссия нашего хука useFriendStatus - подписать нас на статус друга. Вот почему он принимает friendID в качестве аргумента и возвращает информацию о том, находится ли этот друг в сети:


Код
    
  function useFriendStatus(friendID) {
    const [isOnline, setIsOnline] = useState(null);

    // ...

    return isOnline;
  }
  

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




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

Теперь, когда мы извлекли эту логику в хук useFriendStatus, давайте используем ее:


Код
    
  function FriendStatus(props) {
    const isOnline = useFriendStatus(props.friend.id);

    if (isOnline === null) {
      return 'Загрузка...';
    }
    return isOnline ? 'Онлайн' : 'Офлайн';
  }
  


Код
    
  function FriendListItem(props) {
    const isOnline = useFriendStatus(props.friend.id);
  
    return (
      <li style={{ color: isOnline ? 'green' : 'black' }}>
        {props.friend.name}
      </li>
    );
  }
  

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

Должен ли я в названиях своих собственных хуков использовать префикс «use»? Пожалуйста, делайте это. Это соглашение очень важно. Без него мы не смогли бы проверять нарушения правил хуков в автоматическом режиме, так как не смогли бы узнать, содержит ли определенная функция вызовы хуков внутри себя.

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



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


3.12.6.2.1 Подсказка: передавайте информацию между хуками


Поскольку хуки являются функциями, мы можем передавать информацию между ними.

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


Код
    
  const friendList = [
    { id: 1, name: 'Phoebe' },
    { id: 2, name: 'Rachel' },
    { id: 3, name: 'Ross' },
  ];
  
  function ChatRecipientPicker() {
    const [recipientID, setRecipientID] = useState(1);
    const isRecipientOnline = useFriendStatus(recipientID);
  
    return (
      <>
        <Circle color={isRecipientOnline ? 'green' : 'red'} />
        <select
                value={recipientID}
                onChange={e => setRecipientID(Number(e.target.value))}
        >
          {friendList.map(friend => (
            <option key={friend.id} value={friend.id}>
              {friend.name}
            </option>
          ))}
        </select>
      </>
    );
  }
  

Мы сохраняем текущий выбранный идентификатор друга в переменной состояния recepientID и обновляем её, если пользователь выбирает другого друга в элементе <select>.

Поскольку вызов хука useState дает нам последнее значение переменной состояния receientID, мы можем передать её в наш пользовательский хук useFriendStatus в качестве аргумента:


Код
    
  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);
  

Это позволяет нам узнать, находится ли выбранный в данный момент друг в сети. Если мы выберем другого друга и обновим переменную состояния recipientID, наш хук useFriendStatus отменит подписку на ранее выбранного друга и подпишется на статус вновь выбранного.




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

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

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


Код
    
  function todosReducer(state, action) {
    switch (action.type) {
      case 'add':
        return [...state, {
          text: action.text,
          completed: false
        }];
      // ... остальные действия ...
      default:
        return state;
    }
  }
  

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

А что, если бы мы могли написать хук useReducer, который позволит нам управлять локальным состоянием нашего компонента с помощью редюсера? Упрощенная версия может выглядеть так:


Код
    
  function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);

    function dispatch(action) {
      const nextState = reducer(state, action);
      setState(nextState);
    }

    return [state, dispatch];
  }
  

Теперь мы можем использовать локальное состояние в нашем компоненте, а также позволить редюсеру управлять состоянием нашего компонента:


Код
    
  function Todos() {
    const [todos, dispatch] = useReducer(todosReducer, []);

    function handleAddClick(text) {
      dispatch({ type: 'add', text });
    }

    // ...
  }
  

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