Перевод статьи How to Build Your Own React Hooks: A Step-by-Step Guide.

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

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

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

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

Хотите узнать, как создавать собственные хуки React при создании крутых реальных приложений? Переходите по ссылке React Bootcamp .

1. Хук useCopyToClipboard

В предыдущей версии моего сайта reedbarger.com я разрешал пользователям копировать код из моих статей с помощью функции, предоставляемой пакетом react-copy-to-clipboard.

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

Однако вместо того, чтобы использовать стороннюю библиотеку, я захотел воссоздать эту функциональность с помощью моего собственного хука React. Как и в случае с любым создаваемым хуком, я помещаю файл с его кодом в специальную папку, обычно называемую utils или lib. Далее она используется для хранения файлов с разработанными мною функциями, которые я повторно использую в своем проекте.

Код с новым хуком будет находится в файле useCopyToClipboard.js, в который я также помещу функцию с тем же именем.

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

В следующем примере кода мы экспортируем из пакета функцию copy, которую далее при необходимости будем вызывать в своем коде.

// utils/useCopyToClipboard.js
import React from "react";
import copy from "copy-to-clipboard";

export default function useCopyToClipboard() {}

Теперь создадим новую функцию, которая будет использоваться для копирования текстового содержимого, которое будем добавлять в буфер обмена пользователя, и назовем ее handleCopy.

Разрабатываем функцию handleCopy

Рассмотрим последовательно работу этой функции. Первое, что надо сделать – убедиться, что функция принимает в качестве аргументов данные только строкового или числового типа. Для валидации входящих данных и соответствующей их обработки будем использовать блоки кода с инструкцией if-else. В них будем осуществлять проверку следующих типов: строка string или число number. В противном случае будем выводить в консоли сообщение об ошибке, которое информирует пользователя, что он не может копировать другие типы данных.

import React from "react";
import copy from "copy-to-clipboard";

export default function useCopyToClipboard() {
  const [isCopied, setCopied] = React.useState(false);

  function handleCopy(text) {
    if (typeof text === "string" || typeof text == "number") {
      // copy
    } else {
      // don't copy
      console.error(
        `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`
      );
    }
  }
}

После проверки типа, полученное содержимое и преобразуем в строку, которую передаем в импортируемую функцию copy. Затем мы возвращаем функцию handleCopy из хука в любое место нашего приложения.

Функция handleCopy будет связана с обработчиком onClick для соответствующих кнопок.

import React from "react";
import copy from "copy-to-clipboard";

export default function useCopyToClipboard() {
  function handleCopy(text) {
    if (typeof text === "string" || typeof text == "number") {
      copy(text.toString());
    } else {
      console.error(
        `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`
      );
    }
  }

  return handleCopy;
}

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

Начальное значение переменной isCopied будет задано логическим значением false. Если текст был успешно скопирован, то после вызова функции copy установим значение isCopied равным true. В противном случае – false.

И наконец, мы будем возвращать из функции хука массив, содержащий переменную состояния isCopied вместе с функцией handleCopy.

import React from "react";
import copy from "copy-to-clipboard";

export default function useCopyToClipboard(resetInterval = null) {
  const [isCopied, setCopied] = React.useState(false);

  function handleCopy(text) {
    if (typeof text === "string" || typeof text == "number") {
      copy(text.toString());
      setCopied(true);
    } else {
      setCopied(false);
      console.error(
        `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`
      );
    }
  }

  return [isCopied, handleCopy];
}

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

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

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

import React from "react";
import ClipboardIcon from "../svg/ClipboardIcon";
import SuccessIcon from "../svg/SuccessIcon";
import useCopyToClipboard from "../utils/useCopyToClipboard";

function CopyButton({ code }) {
  const [isCopied, handleCopy] = useCopyToClipboard();

  return (
    <button onClick={() => handleCopy(code)}>
      {isCopied ? <SuccessIcon /> : <ClipboardIcon />}
    </button>
  );
}

Добавляем интервал сброса состояния кнопки копирования

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

Если мы захотим сбрасывать состояние кнопки копирования через определенное количество секунд в исходное состояние, то можем передать соответствующий временной интервал в хук useCopyToClipboard. Давайте добавим ему эту полезную функциональность.

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

Добавим стандартный хук useEffect, в который передадим значение интервала сброса состояния resetInterval и переменную isCopied. После успешного копирования и передачи соответствующего значения переменной isCopied, мы запускаем функцию-таймер setTimeout с временем срабатывания равным интервалу сброса, после чего возвращаем переменной isCopied значение false.

Кроме того, необходимо «очистить» созданный объект тайм-аута, для случая если компонент, в котором используется наш хук, размонтируется (то есть состояние для обновления вида кнопки больше хранить не надо).

import React from "react";
import copy from "copy-to-clipboard";

export default function useCopyToClipboard(resetInterval = null) {
  const [isCopied, setCopied] = React.useState(false);

  const handleCopy = React.useCallback((text) => {
    if (typeof text === "string" || typeof text == "number") {
      copy(text.toString());
      setCopied(true);
    } else {
      setCopied(false);
      console.error(
        `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`
      );
    }
  }, []);

  React.useEffect(() => {
    let timeout;
    if (isCopied && resetInterval) {
      timeout = setTimeout(() => setCopied(false), resetInterval);
    }
    return () => {
      clearTimeout(timeout);
    };
  }, [isCopied, resetInterval]);

  return [isCopied, handleCopy];
}

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

Итог

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

import React from "react";
import ClipboardIcon from "../svg/ClipboardIcon";
import SuccessIcon from "../svg/SuccessIcon";
import useCopyToClipboard from "../utils/useCopyToClipboard";

function CopyButton({ code }) {
  // isCopied is reset after 3 second timeout
  const [isCopied, handleCopy] = useCopyToClipboard(3000);

  return (
    <button onClick={() => handleCopy(code)}>
      {isCopied ? <SuccessIcon /> : <ClipboardIcon />}
    </button>
  );
}

2. Хук usePageBottom

В приложениях React в некоторых случаях важно знать, когда пользователь прокрутил страницу до конца.

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

Давайте разберемся на конкретном примере, как создать свой хук usePageBottom, для использования в подобных случаях, например, для реализации «бесконечной» прокрутки.

Как и ранее, начнем с создания в нашей папке utils отдельного файла usePageBottom.js в который добавим функцию (хук) с тем же именем:

// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {}

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

// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {
  React.useEffect(() => {}, []);
}

Пользователь будет прокручивать страницу до конца, то есть до тех пор пока значение свойства window.innerHeight плюс значение свойства document.scrollTop не станет равным document.offsetHeight. Таким образом, если результат проверки этого условия будет истинным, то будем считать пользователь прокрутил страницу до конца:

// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {
  React.useEffect(() => {
    window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight;
  }, []);
}

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

// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {
  const [bottom, setBottom] = React.useState(false);

  React.useEffect(() => {
    const isBottom =
      window.innerHeight + document.documentElement.scrollTop ===
      document.documentElement.offsetHeight;
    setBottom(isButton);
  }, []);

  return bottom;
}

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

Проблема заключается в том, что нам нужно производить вычисления isBottom всякий раз, когда пользователь прокручивает страницу. Это является результатом того, что мы будем постоянно «прослушивать» событие прокрутки окна с помощью метода window.addEventListener. То есть мы будем пересчитывать это выражение, в созданной для этого локальной функции handleScroll, которая будет вызываться всякий раз, когда пользователь прокручивает страницу.

// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {
  const [bottom, setBottom] = React.useState(false);

  React.useEffect(() => {
    function handleScroll() {
      const isBottom =
        window.innerHeight + document.documentElement.scrollTop 
        === document.documentElement.offsetHeight;
      setBottom(isButton);
    }
    window.addEventListener("scroll", handleScroll);
  }, []);

  return bottom;
}

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

Разрешить эту проблему можно возвращая из хука useEffect функцию, в которую обернем метод window.removeEventListener и передадим ему ссылку на функцию handleScroll.

// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {
  const [bottom, setBottom] = React.useState(false);

  React.useEffect(() => {
    function handleScroll() {
      const isBottom =
        window.innerHeight + document.documentElement.scrollTop 
        === document.documentElement.offsetHeight;
      setBottom(isButton);
    }
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  return bottom;
}

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

3. Хук useWindowSize

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

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

Раньше я использовал хук, импортируемый из библиотеки react-use, но вместо того, чтобы каждый раз подключать целую библиотеку, я решил создать свой собственный. Этот хук, который я назвал useWindowSize, при необходимости вычислял бы размеры окна, то есть его ширину и высоту и возвращал эти значения в нужном месте кода.

Разработка хука

Для начала в нашей папке utils создадим новый js файл с тем же именем, что и название хука useWindowSize. Затем я импортирую React, для того чтобы использовать стандартные хуки в коде нашего хука и его корректного экспорта.

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {}

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

То есть для корректной работы хука нам необходимо предусмотреть тот факт, что наш код может работает как в браузере, так и на сервере. Для его определения мы можем проверить соответствие результата выполнения выражения typeof window и строки undefined.

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

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {
  if (typeof window !== "undefined") {
    return { width: 1200, height: 800 };
  }
}

Как получить ширину и высоту окна

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

Чтобы при необходимости узнать ширину и высоту окна браузера, мы можем добавить соответствующий «прослушиватель» события. Например, «прослушивать» событие resize, и всякий раз, когда размеры окна браузера будут изменяться, обновлять часть состояния компонента. А точнее значение переменной windowSize, создаваемой с помощью хука useState, используя для этого функцию-сеттер setWindowSize.

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {
  if (typeof window !== "undefined") {
    return { width: 1200, height: 800 };
  }

  const [windowSize, setWindowSize] = React.useState();

  React.useEffect(() => {
    window.addEventListener("resize", () => {
      setWindowSize({ width: window.innerWidth, height: window.innerHeight });
    });
  }, []);
}

При изменении размера окна будет вызвана функция обратного вызова, и состояние компонента, а точнее значение переменной windowSize будет обновлено с учетом текущих размеров окна браузера. Для этого используются значения ширины и высоты из объекта window.innerWidth и window.innerHeight.

Как добавить поддержку SSR (серверного рендеринга)

Тем не менее с использованием технологии SSR, код в том виде работать не будет. Это связано с тем, что ключевое правило хуков состоит в том, что их нельзя вызывать при необходимости в зависимости от выполнения каких-либо условий. Как результат этого ограничения, мы не можем для их проверки использовать условные операторы до вызова хуков useState, либо useEffect.

Итак, чтобы исправить это, мы отдельно установим начальное значение переменной, использующейся с хуком useState, в зависимости от результатов проверки определенных условий. Для этого создадим переменную isSSR, в которую будем передавать результат выполнения логического выражения, которое проверяет соответствие типа объекта window строке undefined.

Далее мы будем использовать тернарный оператор для установки значений ширины и высоты, предварительно проверив, находимся ли мы на сервере. Если это так, мы будем использовать значение по умолчанию, а если нет, мы будем использовать значения из объекта window.innerWidth и window.innerHeight.

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {
  // if (typeof window !== "undefined") {
  // return { width: 1200, height: 800 };
  // }
  const isSSR = typeof window !== "undefined";
  const [windowSize, setWindowSize] = React.useState({
    width: isSSR ? 1200 : window.innerWidth,
    height: isSSR ? 800 : window.innerHeight,
  });

  React.useEffect(() => {
    window.addEventListener("resize", () => {
      setWindowSize({ width: window.innerWidth, height: window.innerHeight });
    });
  }, []);
}

Затем, наконец, нам нужно подумать о том, что будет когда наш компонент будет отмонтированы. В этом случае нам нужно будет удалять «прослушиватель» события изменения размера окна.

Как удалить «прослушиватель» события изменения размера окна

Вы можете сделать это, вернув соответствующую функцию из хука useEffect. То есть, мы можем удалить «прослушиватель» используя метод window.removeEventListener следующим образом.

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {
  // if (typeof window !== "undefined") {
  // return { width: 1200, height: 800 };
  // }
  const isSSR = typeof window !== "undefined";
  const [windowSize, setWindowSize] = React.useState({
    width: isSSR ? 1200 : window.innerWidth,
    height: isSSR ? 800 : window.innerHeight,
  });

  React.useEffect(() => {
    window.addEventListener("resize", () => {
      setWindowSize({ width: window.innerWidth, height: window.innerHeight });
    });

    return () => {
      window.removeEventListener("resize", () => {
        setWindowSize({ width: window.innerWidth, height: window.innerHeight });
      });
    };
  }, []);
}

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

И наконец, в конце кода хука вернем значение переменной состояния windowSize.

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {
  const isSSR = typeof window !== "undefined";
  const [windowSize, setWindowSize] = React.useState({
    width: isSSR ? 1200 : window.innerWidth,
    height: isSSR ? 800 : window.innerHeight,
  });

  function changeWindowSize() {
    setWindowSize({ width: window.innerWidth, height: window.innerHeight });
  }

  React.useEffect(() => {
    window.addEventListener("resize", changeWindowSize);

    return () => {
      window.removeEventListener("resize", changeWindowSize);
    };
  }, []);

  return windowSize;
}

Итог

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

Примем за фиксированное значение ширины отметку в 500 пикселей. При изменении размеров окна менее этого значения – скрываем все ссылки в меню и показываем только кнопку «Join Now», как в примере выше:

// components/StickyHeader.js

import React from "react";
import useWindowSize from "../utils/useWindowSize";

function StickyHeader() {
  const { width } = useWindowSize();

  return (
    <div>
      {/* visible only when window greater than 500px */}
      {width > 500 && (
        <>
          <div onClick={onTestimonialsClick} role="button">
            <span>Testimonials</span>
          </div>
          <div onClick={onPriceClick} role="button">
            <span>Price</span>
          </div>
          <div>
            <span onClick={onQuestionClick} role="button">
              Question?
            </span>
          </div>
        </>
      )}
      {/* visible at any window size */}
      <div>
        <span className="primary-button" onClick={onPriceClick} role="button">
          Join Now
        </span>
      </div>
    </div>
  );
}

Этот хук будет также корректно работать с любым серверным приложением React, таким как Gatsby и Next.js.

4. Хук useDeviceDetect

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

Я отследил причину проблемы до одной сторонней библиотеки под названием response-device-detect, которую я использовал, чтобы определять, использует ли пользователь для просмотра сайта мобильное устройство или нет. И если это так, я бы убирал слишком большой заголовок из-за которого ломалась страница.

// templates/course.js
import React from "react";
import { isMobile } from "react-device-detect";

function Course() {
  return (
    <>
      <SEO />
      {!isMobile && <StickyHeader {...courseData} />}
      {/* more components... */}
    </>
  );
}

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

Работа над кодом хука

Как и ранее я создал в моей папке utils отдельный файл с тем же именем, useDeviceDetect.js. И поскольку хуки – это просто функции JavaScript для общего назначения, которые используются механизмом хуков в React, я создаю функцию с тем же именем useDeviceDetect и импортирую React.

// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {}

Как получить информацию о пользовательском агенте user agent

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

А поскольку взаимодействие с интерфейсом окна браузера window API, как и с API внешнего ресурса можно в общем случае рассматривать как побочный эффект, то мы можем получить доступ к информации о пользовательском агенте внутри кода хука useEffect.

// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {
  React.useEffect(() => {
    console.log(`user's device is: ${window.navigator.userAgent}`);
    // can also be written as 'navigator.userAgent'
  }, []);
}

После монтирования компонента используем инструкцию кода typeof navigator, чтобы определить, выполняется код в браузере или же на сервере. Если мы находимся на сервере, то у нас соответственно не будет доступа к объекту window. Поэтому результат выполнения выражения typeof navigator будет эквивалентен строке undefined, поскольку этого свойства не существует. В противном случае (код выполняется в браузере) мы можем получить значение свойства, содержащего информацию о пользовательском агенте, способом описанном выше.

Используем тернарный оператор для получения данных userAgent следующим образом:

// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {
  React.useEffect(() => {
    const userAgent =
      typeof navigator === "undefined" ? "" : navigator.userAgent;
  }, []);
}

Как определить, соответствует ли значение userAgent мобильному устройству

Значение свойства userAgent представляет собой строку, которая содержит следующие имена устройств, соответствующие мобильным устройствам: Android, BlackBerry, iPhone, iPad, iPod, Opera Mini, IEMobile или WPDesktop.

И всё, что нам нужно сделать это, используя полученную строку и метод match() с соответствующим регулярным выражением, проверить содержит ли она одну из перечисленных строк. Логическое значение, полученное в результате выполнения проверки сохраним в локальной переменной с именем mobile.

Далее сохраним полученный результат в состоянии компонента, используя хук useState, которому при вызове присвоим начальное значение false. Для этого создадим переменную состояния isMobile, и соответствующую ей функцию-сеттер setMobile.

// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {
  const [isMobile, setMobile] = React.useState(false);

  React.useEffect(() => {
    const userAgent =
      typeof window.navigator === "undefined" ? "" : navigator.userAgent;
    const mobile = Boolean(
      userAgent.match(
        /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
      )
    );
    setMobile(mobile);
  }, []);
}

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

В возвращаемый объект добавим значение isMobile:

// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {
  const [isMobile, setMobile] = React.useState(false);

  React.useEffect(() => {
    const userAgent =
      typeof window.navigator === "undefined" ? "" : navigator.userAgent;
    const mobile = Boolean(
      userAgent.match(
        /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
      )
    );
    setMobile(mobile);
  }, []);

  return { isMobile };
}

Итог

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

// templates/course.js
import React from "react";
import useDeviceDetect from "../utils/useDeviceDetect";

function Course() {
  const { isMobile } = useDeviceDetect();

  return (
    <>
      <SEO />
      {!isMobile && <StickyHeader {...courseData} />}
      {/* more components... */}
    </>
  );
}

Заключение

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

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

Оставить комментарий