Рубріки: ОсновыТеория

Что такое хуки и как их использовать: краткий гайд с примерами

Ольга Змерзла

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

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

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

Встроенные хуки

Хуки можно создавать самостоятельно, но в React существует ряд встроенных хуков, которыми можно пользоваться прямо сейчас.

useState()

Хук состояния — useState() — добавляет динамическую логику функциональных компонентов:

const [state, setState] = useState(initialState);

Хук возвращает функцию и значение состояния для обновления.

Пример. Рендеринг счетчика. При нажатии на кнопку его значение увеличится:

import React, { useState } from 'react';

function Example() {

 // Объявляем новую переменную состояния "count"
 const [count, setCount] = useState(0);

  return (
    <div>
      <p>Вы нажали {count} раз</p>
      <button onClick={() => setCount(count + 1)}>
        Нажми на меня
      </button>
    </div>
  );
}

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

useCallback()

Хук useCallback()  оптимизирует рендеринг компонентов:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

А также возвращает мемоизированную версию Callback (когда результат выполнения сохраняется). При этом массив зависимостей не передается.

useContext()

Хук useContext() передает данные дочерним элементам:

const value = useContext(MyContext);

А также возвращает текущее значение контекста.

useMemo()

Хук useMemo() позволяет запоминать функции без необходимости их вызова при каждом новом рендеринге:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

А еще — возвращает мемоизированное значение.

Функция, которая передается хуком useMemo, запускается при рендере.

useEffect()

Хук эффекта — useEffect() — выполняет в компонентах функций такие эффекты как вызов API, запросов и подобные:

useEffect(didUpdate);

Функция, которую передает useEffect(), запускается только после фиксации рендера на экране.

Пример. Установка компонентом заголовка документа после обновления DOM:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);


 // По принципу componentDidMount и componentDidUpdate:
 useEffect(() => {
   // Обновляем заголовок документа, используя API браузера
   document.title = `Вы нажали ${count} раз`;
 });

  return (
    <div>
      <p>Вы нажали {count} раз</p>
      <button onClick={() => setCount(count + 1)}>
        Нажми на меня
      </button>
    </div>
  );
}

React запускает функции с эффектами после каждого рендера.

useReduce()

Хук useReduce()  — альтернатива хуку useState(), но используется для сложной логики состояния, управляет внутренним состоянием более сложного компонента:

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

useRef()

Хук useRef()создает изменяемую переменную (упрощает доступ к элементам React и узлам DOM):

const refContainer = useRef(initialValue);

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

Пример. Доступ к потомку:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` указывает на смонтированный элемент `input`
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Установить фокус на поле ввода</button>
    </>
  );
}

Хук useRef() содержит изменяемое значение .current. При каждом рендере хук useRef() дает один и тот же объект с ref.

useImperativeHandle()

Хук useImperativeHandle() настраивает объект при использовании ref, передающийся родительскому компоненту.

Пример. Использование useImperativeHandle с forwardRef:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}

FancyInput = forwardRef(FancyInput)

Хук useImperativeHandle() контролирует возвращаемое значение. Довольно часто этот хук используют в библиотеках компонентов, где нужна кастомизация поведения элементов DOM.

useDebugValue()

Хук useDebugValue() отображает значение для отладки.

Пример. Создание своего React-хука:

import React, {useDebugValue, useState, useEffect} from 'react'

function useRandomNumber(min, max) {
  const [number, setNumber] = useState(null);

  useEffect(() => {
    const range = Math.random() * (max - min) + min
    setNumber(Math.floor(range))
  }, [])

  useDebugValue(number > 50 ? 'Number more the 50' : 'Number less then 50');

  return number;
}

const UseDebugValuePage = () => {
  const number = useRandomNumber(1, 100)

  return (
    <div>
      <h2>useDebugValue</h2>
      <p>Random number: {number}</p>
    </div>
  )
}

export default UseDebugValuePage

Хук useDebugValue() будет вызван тогда, когда будет открыт React DevTools.

useLayoutEffect()

Хук useLayoutEffect() — альтернатива хука useEffect(), но вызывается в DOM после изменений в структуре:

import React, {useEffect, useLayoutEffect} from 'react'

const UseLayoutEffectPage = () => {
  const ref = React.useRef()

  useEffect(() => {
    ref.current.value = 'New value'
    console.log('useEffect');
  })

  useLayoutEffect(() => {
    console.log(ref.current.value)
  })

  return (
    <div>
      <h2>useLayoutEffect</h2>
      <input ref={ref} value='Old value' />
    </div>
  )
}

export default UseLayoutEffectPage

Хук useLayoutEffect() запускается в React только после фиксации в DOM всех обновлений.

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

Хуки — это тоже функции JavaScript, но чтобы их правильно использовать, нужно следовать кое-каким правилам:

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

Как создать хуки?

Пусть в компоненте, принадлежащем чату (приложению), отображается уведомление о нахождении пользователя «В сети»:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {

 const [isOnline, setIsOnline] = useState(null);
 useEffect(() => {
   function handleStatusChange(status) {
     setIsOnline(status.isOnline);
   }
   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);  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline);    }    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 props;
  • компоненты высшего порядка.

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

Разделить логику между двумя функциями JavaScript можно, если извлечь ее в еще одну, третью функцию.

Первый пользовательский хук — useFriendStatus():

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

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

  return isOnline;
}

Этот хук подписывает нас на статус пользователя. 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 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

Что здесь произошло? Мы извлекли код из двух функций и поместили в отдельную функцию.

Важно! Используйте useв начале каждого хука. Это нужно для автоматической проверки правил хуков.

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

Как мы уже говорили, хуки — это те же функции. А потому можно передавать информацию между ними.

Давайте рассмотрим пример использования компонента, демонстрирующего, находится ли пользователь сейчас онлайн:

const friendList = [
  { id: 1, name: 'Ольга' },
  { id: 2, name: 'Жанна' },
  { id: 3, name: 'Елизавета' },
];

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>
    </>
  );
}

Идентификатор пользователя сохраняется в recipientID и обновляется в случае выбора в <select> другого пользователя.

Хук useState() возвращает значение recipientID, а значит его можно передать в useFriendStatus в качестве аргумента:

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

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

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

Ограничения при использовании хуков

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

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

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

React определяет порядок, при котором вызываются хуки. Именно так он узнает, какому useState() какое состояние соответствует. Поскольку в примере порядок вызовов одинаков при каждом рендере, он является рабочим:

// ------------
// First render
// ------------
useState('Mary')           // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm)     // 2. Add an effect for persisting the form
useState('Poppins')        // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle)     // 4. Add an effect for updating the title

// -------------
// Second render
// -------------
useState('Mary')           // 1. Read the name state variable (argument is ignored)
useEffect(persistForm)     // 2. Replace the effect for persisting the form
useState('Poppins')        // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle)     // 4. Replace the effect for updating the title

// ...

Но что получится, если поместить хук в условие?

// 🔴 We're breaking the first rule by using a Hook in a condition
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

Условие (name !== '') имеет статус true для первого рендера. Запускаем хук. Но при следующем рендеринге пользователь может очистить форму, и тогда условие будет false. Порядок вызовов хука изменен:

useState('Mary')           // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm)  // 🔴 This Hook was skipped!
useState('Poppins')        // 🔴 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle)     // 🔴 3 (but was 4). Fail to replace the effect

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

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

function ClickButtonHook(props){
    const [count, setCount] = React.useState(0);
 
    const press= () => setCount(count + props.increment);
    return <div>
            <button onClick={press}>Count</button>
            <div>Counter: {count}<br /> Increment: {props.increment}</div>
    </div>;
}

Также они могут определяться в виде стрелочных функций:

const ClickButtonHook = (props)=>{
    const [count, setCount] = React.useState(0);
 
    const press= () => setCount(count + props.increment);
    return <div>
            <button onClick={press}>Count</button>
            <div>Counter: {count}<br /> Increment: {props.increment}</div>
    </div>;
}

Как подключать хуки

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

import React, { useState } from "react";
 
function ClickButtonHook(props){
    const [count, setCount] = React.useState(0);
 
    const press= () => setCount(count + props.increment);
    return <div>
            <button onClick={press}>Count</button>
            <div>Counter: {count}<br /> Increment: {props.increment}</div>
    </div>;
}

Заключение

В сравнении с классами, у хуков есть ряд своих преимуществ. Упрощается логика жизненного цикла, появляются возможности гибкой оптимизации (мемоизация), нет необходимости сохранять имена методов и работать с прототипами.

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

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

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

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

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023