前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >亲手打造属于你的 React Hooks

亲手打造属于你的 React Hooks

作者头像
前端修罗场
发布2022-07-29 08:02:29
10.1K0
发布2022-07-29 08:02:29
举报
文章被收录于专栏:Web 技术

自定义 React Hook 是一个必要的工具,它可以让你为 React 应用程序添加特殊的、独特的功能。

在许多情况下,如果你想向应用程序添加特定的特性,您可以简单地安装一个第三方库来解决您的问题。但如果这样的库或钩子不存在,该怎么办?

作为 React 开发人员,学习如何创建自定义钩子来解决问题或在自己的 React 项目中添加缺失的特性是很重要的。

在这个循序渐进的指南中,我将通过分解我为自己的应用程序创建的三个钩子,以及创建这些钩子是为了解决什么问题,向您展示如何创建自己的自定义React钩子。

useCopyToClipboard Hook

在我以前的网站上,我允许用户在一个名为 react-copy-to-clipboard 的包的帮助下从我的文章中复制代码。

用户只需将鼠标悬停在代码片段上,单击剪贴板按钮,代码就会被添加到他们电脑的剪贴板中,以便他们可以在任何他们想要的地方粘贴和使用代码。

然而,我不想使用第三方库,而是想用自己的自定义 React 钩子重新创建这个功能。对于我创建的每个自定义 react 钩子,我都把它放在一个专门的文件夹中,通常称为 utils 或 lib,专门用于我可以在应用程序中重用的函数。

我们将把这个钩子放到一个名为 useCopyToClipboard.js 的文件中,并创建一个同名的函数。

我们有多种方法可以将一些文本复制到用户的剪贴板。我更喜欢使用一个库来实现这一点,这个库使这个过程更加可靠,这个库叫做“复制到剪贴板”。

它导出一个函数,我们将其称为copy。

代码语言:javascript
复制
// utils/useCopyToClipboard.js
import React from "react";
import copy from "copy-to-clipboard";

export default function useCopyToClipboard() {}

接下来,我们将创建一个函数,用于复制想要添加到用户剪贴板的任何文本。我们将调用这个函数 handleCopy。

handleCopy

在这个函数中,我们首先需要确保它只接受字符串或数字类型的数据。我们将建立一个 if-else 语句,它将确保类型是字符串或数字。否则,我们将在控制台 log 一个 error,告诉用户不能复制任何其他类型。

代码语言:javascript
复制
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") {
      // 复制
    } else {
      // 不复制
      console.error(
        `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`
      );
    }
  }
}

接下来,我们获取文本并将其转换为字符串,然后将其传递给 copy 函数。从那里,我们将 handleCopy 函数从钩子返回到应用程序中我们想要的任何地方。

通常,handleCopy函数会连接到一个按钮的onClick。

代码语言:javascript
复制
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,并创建一个新的状态变量 iscopy ,其中的 setter将被称为 setCopy

最初这个值是假的。如果文本成功复制,我们将把 copy 设置为 true。否则,我们将它设置为 false

最后,在数组中返回 isreplicate from the hook with handleCopy

代码语言:javascript
复制
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。

在我的例子中,我将使用它与一个复制按钮组件,它接收我们的代码片段的代码。

要做到这一点,我们需要做的就是向按钮添加一个onclick。并在返回一个名为handle的函数时,将被请求的代码复制为文本。一旦复制成功,就是真的了。我们可以显示一个不同的图标,表明复制成功。

代码语言:javascript
复制
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>
  );
}
增加重置时间间隔

我们可以对代码做一个改进。就像我们现在所编写的钩子一样,iscopy总是正确的,这意味着我们总是能够看到成功图标。

如果我们想在几秒钟后重置我们的状态,你可以传递一个时间间隔给useCopyToClipboard。让我们添加这个功能。

回到我们的钩子中,我们可以创建一个名为 resetInterval 的形参,它的默认值为null,这将确保在没有参数传递给它的情况下状态不会重置。

然后,我们添加 useEffect,说明如果文本被复制,并且我们有一个重置间隔,我们将在这个间隔之后使用 setTimeoutisCopied设为false

此外,如果钩子所使用的组件正在卸载(这意味着我们的状态不再需要更新),我们需要清除这个超时。

代码语言:javascript
复制
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钩子中,以确保它不会在每次有重新渲染时被重新创建。

结果

有了那个,我们有了我们的最终钩子它允许状态在给定的时间间隔后被重置。如果我们传递一个给它,我们应该看到如下所示的结果。

代码语言:javascript
复制
import React from "react";
import ClipboardIcon from "../svg/ClipboardIcon";
import SuccessIcon from "../svg/SuccessIcon";
import useCopyToClipboard from "../utils/useCopyToClipboard";

function CopyButton({ code }) {
  // isCopied 在3秒超时后被重置
  const [isCopied, handleCopy] = useCopyToClipboard(3000);

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

usePageBottom Hook

在 React 应用中,有时了解用户何时滚动到页面底部是很重要的。

在你可以无限滚动的应用中,比如微博,一旦用户点击页面底部,你就需要获取更多的帖子。

让我们看看如何自己创建一个 usePageBottom钩子,用于类似的用例,比如创建无限滚动。

我们将从创建一个单独的文件usePageBottom开始。我们将在我们的utils文件夹中添加一个同名的函数(hook):

代码语言:javascript
复制
// utils/usePageBottom.js
import React from "react";

export default function usePageBottom() {}

接下来,我们需要计算用户何时到达页面底部。我们可以通过窗口的信息来确定。为了访问它,我们需要确保钩子在内部被调用的组件被挂载,所以我们将使用一个空的dependencies数组的useEffect钩子。

代码语言:javascript
复制
// utils/usePageBottom.js
import React from "react";

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

当窗口的innerHeight值加上文档的scrollTop值等于offsetHeight值时,用户将滚动到页面的底部。如果这两个值相等,结果将为真,并且用户已经滚动到页面底部:

代码语言:javascript
复制
// utils/usePageBottom.js
import React from "react";

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

我们将把这个表达式的结果存储在变量isBottom中,并更新一个名为bottom的状态变量,这个状态变量最终会从钩子中返回。

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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;
}

最后,因为我们有一个正在更新状态的事件侦听器,所以我们需要处理用户从页面导航离开和组件被删除的事件。我们需要删除添加的滚动事件监听器,这样就不会尝试更新不再存在的状态变量。

我们可以通过从useEffectwindow返回一个函数来实现这一点。removeEventListener,其中我们传递了对同一个handleScroll函数的引用。

代码语言:javascript
复制
// 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;
}

现在,我们可以在任何想知道是否已经到达页面底部的函数中简单地调用这段代码。

为此,我们可以使用一个媒体查询(CSS),或者使用一个自定义的React钩子来提供当前页面的大小,并隐藏或显示JSX中的链接。

以前,我使用的是一个名为react-use的库中的钩子。我决定创建自己的钩子来提供窗口的尺寸,包括宽度和高度,而不是引入整个第三方库。我把这个钩子叫做useWindowSize

useWindowSize

首先,我们将在utils文件夹中创建一个新的.js文件,与钩子useWindowSize同名。我将在导出自定义钩子的同时导入React(以使用钩子)。

代码语言:javascript
复制
// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {}

为了检查并确保我们不在服务器上,我们可以查看type of window是否等于字符串undefined。

在这种情况下,我们可以为浏览器返回默认的宽度和高度,例如,在一个对象内1200和800:

代码语言:javascript
复制
// utils/useWindowSize.js

import React from "react";

export default function useWindowSize() {
  if (typeof window !== "undefined") {
    return { width: 1200, height: 800 };
  }
}
如何从窗口得到宽度和高度

假设我们在客户端并且可以获得窗口,我们可以使用useEffect钩子通过与窗口交互来执行一个副作用。我们将包含一个空的dependencies数组,以确保effect函数只在组件(调用这个钩子的组件)挂载之后才被调用。

为了找出窗口的宽度和高度,我们可以添加一个事件监听器来监听resize事件。当浏览器大小改变时,我们可以更新一块状态(用useState创建),我们将其称为windowSize,更新它的setter将是setWindowSize

代码语言:javascript
复制
// 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状态将根据当前窗口尺寸更新。为了得到它,我们设置width=window.innerWidth, height=window.innerHeight

添加SSR支持

然而,我们这里的代码将不能工作。这是因为hook的一个关键规则是不能有条件地调用它们。因此,在useStateuseEffect钩子被调用之前,不能有一个条件钩子。

为了解决这个问题,我们将有条件地设置useState的初始值。我们将创建一个名为isSSR的变量,它将执行相同的检查,以查看窗口是否等于未定义的字符串。

我们将使用三元值来设置宽度和高度首先检查我们是否在服务器上。如果是,则使用默认值,如果不是,则使用window.innerWidth window.innerHeight

代码语言:javascript
复制
// 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,
  });

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

最后,我们需要考虑组件何时卸载。我们需要做什么?我们需要删除调整大小监听器。

如何删除 resize 事件监听器

你可以通过从useEffect 返回一个函数来做到这一点。我们将使用window.removeEventListener删除侦听器。

代码语言:javascript
复制
// 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,
  });

  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状态。

代码语言:javascript
复制
// 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;
}
结果

要使用钩子,我们只需要在需要的地方导入它,调用它,并在想要隐藏或显示某些元素的地方使用宽度。

在我的例子中,这是500px标记。在那里,我想隐藏所有其他链接,只显示Join Now按钮,就像你在上面的例子中看到的:

代码语言:javascript
复制
// components/StickyHeader.js

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

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

  return (
    <div>
      {/* 仅当窗口大于500px时可见 */}
      {width > 500 && (
        <>
          <div onClick={onTestimonialsClick} role="button">
            <span>奖状</span>
          </div>
          <div onClick={onPriceClick} role="button">
            <span>价格</span>
          </div>
          <div>
            <span onClick={onQuestionClick} role="button">
             问题?
            </span>
          </div>
        </>
      )}
      {/* 在任何大小的窗口可见 */}
      <div>
        <span className="primary-button" onClick={onPriceClick} role="button">
          现在加入
        </span>
      </div>
    </div>
  );
}

这个钩子将在任何服务器渲染的React应用程序上工作,比如 Next.js。

useDeviceDetect Hook

我正在构建一个新的登录页面时,我在移动设备上经历了一个非常奇怪的错误。在台式电脑上,这些样式看起来很棒。

但当我着眼于移动平台时,我发现所有内容都是不合适的,并且都是破碎的。

我追踪这个问题到一个名为react-device-detect的库,我用它来检测用户是否有移动设备。如果是,我将删除标题。

代码语言:javascript
复制
// templates/course.js
import React from "react";
import { isMobile } from "react-device-detect";

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

问题是这个库不支持服务器端呈现,而这是默认使用的。所以我需要创建自己的解决方案来检查用户何时使用移动设备。为此,我决定创建一个名为useDeviceDetect的自定义钩子。

创建 useDeviceDetect

我在我的utils文件夹中用相同的名字为这个钩子创建了一个单独的文件useDeviceDetect.js。因为钩子只是可共享的JavaScript函数,它利用React钩子,所以我创建了一个名为useDeviceDetect的函数并导入了React。

代码语言:javascript
复制
// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {}
如何从window获得用户代理

我们可以确定是否可以获得关于用户设备的信息的方法是通过userAgent属性(位于windownavigator属性上)。

由于与作为API /外部资源的窗口API交互将被归类为副作用,所以我们需要访问useEffect钩子中的用户代理。

代码语言:javascript
复制
// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {
  React.useEffect(() => {
    console.log(`user's device is: ${window.navigator.userAgent}`);
    // 也可以写成'navigator.userAgent'
  }, []);
}

一旦组件安装完毕,我们就可以使用typeof navigator来确定我们是在客户机上还是在服务器上。如果我们在服务器上,我们就无法进入窗口。typeof navigator将等于未定义的字符串,因为它不存在。否则,如果我们在客户机上,我们将能够获得我们的用户代理属性。

我们可以使用三元制来表示所有这些,以获得userAgent数据:

代码语言:javascript
复制
// utils/useDeviceDetect.js
import React from "react";

export default function useDeviceDetect() {
  React.useEffect(() => {
    const userAgent =
      typeof navigator === "undefined" ? "" : navigator.userAgent;
  }, []);
}
如何检查userAgent是否是移动设备

userAgent是一个字符串值,如果使用移动设备,它将被设置为以下设备名中的任何一个:

Android,iPhone, iPad, iPod, Opera Mini, imobile,或WPDesktop。

我们所要做的就是获取我们得到的字符串,并使用.match()方法和一个regex来查看它是否是这些字符串中的任何一个。我们将它存储在一个叫做mobile的局部变量中。

我们将结果存储在useState钩子的状态中,并将初始值赋给它false。对于它,我们将创建一个相应的状态变量isMobile, setter将是setMobile。

代码语言:javascript
复制
// 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);
  }, []);
}

一旦我们获得了移动值我们就会将它设置为状态。最后,我们将从该钩子返回一个对象,这样如果我们想给该钩子添加更多的功能,就可以在将来添加更多的值。

在对象中,我们将添加isMobile作为属性和值:

代码语言:javascript
复制
// 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 };
}
结果

回到登录页,我们可以执行钩子,并从解构对象中获得该属性,并在需要的地方使用它。

代码语言:javascript
复制
// 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钩子。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端修罗场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • useCopyToClipboard Hook
    • handleCopy
      • 使用 useCopyToClipboard
        • 增加重置时间间隔
          • 结果
          • usePageBottom Hook
            • useWindowSize
              • 如何从窗口得到宽度和高度
                • 添加SSR支持
                  • 如何删除 resize 事件监听器
                    • 结果
                    • useDeviceDetect Hook
                      • 创建 useDeviceDetect
                        • 如何从window获得用户代理
                          • 如何检查userAgent是否是移动设备
                            • 结果
                            • 总结
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档