Kodokon kodokon.com

Свои хуки: выносим и переиспользуем логику

Выноси логику с состоянием в переиспользуемые, компонуемые и тестируемые хуки.

8 мин · 3 вопросов

Открыть этот урок в Kodokon

Свой хук - это функция, имя которой начинается с use и которая вызывает другие хуки. Он выносит логику с состоянием, а не общее состояние: каждый компонент, который его вызывает, получает свои собственные useState и useEffect, полностью изолированные. Это самый прямой инструмент React для вынесения логики - там, где HOC и render props добавляли слои в дерево, хук просто встраивается в тело компонента, легко компонуется и легко тестируется.

JAVASCRIPT
import { useEffect, useState } from "react";

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => {
      setDebounced(value);
    }, delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
Функция очистки отменяет предыдущий таймер при каждом изменении.

Префикс use не для красоты: именно он позволяет ESLint-плагину react-hooks проверять правила хуков внутри твоей функции - вызовы без условий, на верхнем уровне. Позаботься и о форме возвращаемого API: кортеж [value, setValue], когда использование повторяет useState, и объект с именованными ключами, как только наружу нужно отдать больше двух вещей.

JAVASCRIPT
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored !== null
      ? JSON.parse(stored)
      : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}
Ленивая инициализация: localStorage читается только при монтировании.

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

JAVASCRIPT
function useUserSearch(query) {
  const debounced = useDebouncedValue(query, 400);
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (debounced === "") {
      setResults([]);
      return;
    }
    const controller = new AbortController();
    fetch("/api/search?q=" + debounced, {
      signal: controller.signal,
    })
      .then((res) => res.json())
      .then(setResults)
      .catch(() => {});
    return () => controller.abort();
  }, [debounced]);
  return results;
}
Бизнес-хук строится поверх универсального хука.

Проверка знаний

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

  1. Для чего на самом деле нужен префикс use у своего хука?
    • Это чисто косметическое соглашение, доставшееся от классов
    • Он позволяет линтеру опознать функцию как хук и проверить внутри неё правила хуков
    • React отказывается выполнять функцию с состоянием без этого префикса
    • Он запускает автоматический импорт из react
  2. Два компонента вызывают useLocalStorage с одним и тем же ключом. Разделяют ли они состояние?
    • Да: хук автоматически синхронизирует всех, кто его вызывает
    • Нет, и данные сохраняются под двумя разными ключами хранилища
    • Нет: у каждого вызова своё состояние React; общими остаются только данные в localStorage
  3. Какова роль функции очистки, которую возвращает useEffect в useDebouncedValue?
    • Отменить ожидающий таймер, чтобы после задержки публиковалось только последнее введённое значение
    • Сбрасывать отложенное значение при каждом размонтировании компонента
    • Блокировать перерисовки компонента на всё время задержки