Как создать перехватчики React: пошаговое руководство

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

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

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

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

Хотите узнать, как создавать собственные перехватчики React при создании забавных реальных приложений? Ознакомьтесь с The 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, который будет гарантировать, что тип является либо строкой, либо строкой, либо числом. В противном случае мы будем регистрировать ошибку в консоли, которая сообщает пользователю, что вы не можете копировать любые другие типы.

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.

Изначально это значение будет ложным. Если текст успешно скопирован. Мы установим copy в значение 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

Теперь мы можем использовать useCopyToClipboard в любом компоненте, который нам нравится.

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

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

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, чтобы сказать, что если текст скопирован и у нас есть интервал сброса, мы установим isCopied обратно в false после этого интервала, используя setTimeout.

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

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 для аналогичных случаев использования, таких как создание бесконечной прокрутки.

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

// 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(() => {}, []);
}

Пользователь будет прокручивать страницу вниз, когда значение окна innerHeight плюс значение документа scrollTop равно 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;
}

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

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

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

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

Создание крючка

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

// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {}

Теперь, поскольку я использую это на сайте Gatsby, который обрабатывается сервером, мне нужно получить размер окна, но у нас может не быть к нему доступа, потому что мы находимся на сервере. Чтобы проверить и убедиться, что мы не на сервере, мы можем увидеть, не совпадает ли тип window со строкой undefined.

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

// utils/useWindowSize.js

import React from "react";

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

Получение ширины и высоты из окна

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

Чтобы узнать ширину и высоту окна, мы можем добавить прослушиватель событий и прослушивать событие resize. И всякий раз, когда размеры браузера меняются, мы можем обновить часть состояния (созданную с помощью useState), которую мы назовем windowSize, а установщик для ее обновления будет 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

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

Чтобы исправить это, мы условно установим начальное значение useState. Мы создадим переменную с именем isSSR, которая будет выполнять ту же проверку, чтобы увидеть, не совпадает ли окно со строкой 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 пикселей. Здесь я хочу скрыть все остальные ссылки и показать только кнопку Присоединиться сейчас, как вы видите в примере выше:

// 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.

3. Перехватчик useDeviceDetect

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

Но когда я посмотрел на мобильный, все было не на своем месте и сломано.

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

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

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

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

Создание крючка

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

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

export default function useDeviceDetect() {}

Получение пользовательского агента из окна

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

А поскольку взаимодействие с оконным 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, чтобы определить, находимся мы на клиенте или на сервере. Если мы на сервере, у нас не будет доступа к окну. 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.

Понравился этот пост? Присоединяйтесь к React Bootcamp

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

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

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