首页 > web前端 > js教程 > 正文

如何构建自己的 React Hook:分步指南

王林
发布: 2024-08-09 14:32:36
原创
631 人浏览过

How to Build Your Own React Hooks: A Step-by-Step Guide

自定义 React 挂钩是一个重要的工具,可让您向 React 应用程序添加特殊、独特的功能。

在许多情况下,如果您想向应用程序添加某个功能,您只需安装专门为解决您的问题而设计的第三方库即可。但如果这样的库或钩子不存在,你该怎么办?

作为一名 React 开发人员,学习创建自定义挂钩来解决问题或在自己的 React 项目中添加缺失功能的过程非常重要。

在本分步指南中,我将向您展示如何通过分解我为自己的应用程序制作的三个钩子来创建您自己的自定义 React 钩子,以及创建它们是为了解决哪些问题。

  1. useCopyToClipboard 钩子 在我的网站 reedbarger.com 的过去版本中,我允许用户借助名为“react-copy-to-clipboard”的包从我的文章中复制代码。

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

复制-gif.gif
然而,我不想使用第三方库,而是想使用我自己的自定义 React hook 重新创建此功能。与我创建的每个自定义 React hook 一样,我将其放在一个专用文件夹中,通常称为 utils 或 lib,专门用于我可以在应用程序中重用的函数。

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

我们可以通过多种方式将一些文本复制到用户的剪贴板。我更喜欢为此使用一个库,这使得该过程更加可靠,称为复制到剪贴板。

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

// utils/useCopyToClipboard.js
从“react”导入React;
从“复制到剪贴板”导入副本;

导出默认函数useCopyToClipboard() {}
接下来,我们将创建一个函数,用于复制要添加到用户剪贴板的任何文本。我们将调用这个函数handleCopy。

如何制作handleCopy函数
在函数中,我们首先需要确保它只接受字符串或数字类型的数据。我们将设置一个 if-else 语句,这将确保类型是字符串或数字。否则,我们将在控制台记录一条错误,告诉用户您无法复制任何其他类型。

从“react”导入React;
从“复制到剪贴板”导入副本;

导出默认函数useCopyToClipboard() {
const [isCopied, setCopied] = React.useState(false);

函数handleCopy(text) {
if (typeof text === "string" || typeof text == "number") {
// 复制
} 其他 {
// 不要复制
控制台.错误(
无法将 typeof ${typeof text} 复制到剪贴板,必须是字符串或数字。
);
}
}
}
接下来,我们获取文本并将其转换为字符串,然后将其传递给复制函数。从那里,我们将 handleCopy 函数从钩子返回到应用程序中我们喜欢的任何位置。

一般来说,handleCopy函数会连接到按钮的onClick。

从“react”导入React;
从“复制到剪贴板”导入副本;

导出默认函数useCopyToClipboard() {
函数handleCopy(text) {
if (typeof text === "string" || typeof text == "number") {
复制(text.toString());
} 其他 {
控制台.错误(
无法将 typeof ${typeof text} 复制到剪贴板,必须是字符串或数字。
);
}
}

返回句柄复制;
}
此外,我们需要一些状态来表示文本是否被复制。为了创建它,我们将在钩子顶部调用 useState 并创建一个新的状态变量 isCopied,其中 setter 将被称为 setCopy。

最初该值将为 false。如果文本复制成功,我们将把 copy 设置为 true。否则,我们将其设置为 false。

最后,我们将从数组中的钩子中返回 isCopied 以及 handleCopy。

从“react”导入React;
从“复制到剪贴板”导入副本;

导出默认函数useCopyToClipboard(resetInterval = null) {
const [isCopied, setCopied] = React.useState(false);

函数handleCopy(text) {
if (typeof text === "string" || typeof text == "number") {
复制(text.toString());
setCopied(true);
} 其他 {
setCopied(假);
控制台.错误(
无法将 typeof ${typeof text} 复制到剪贴板,必须是字符串或数字。
);
}
}

返回[isCopied,handleCopy];
}
如何使用useCopyToClipboard
我们现在可以在任何我们喜欢的组件中使用 useCopyToClipboard。

就我而言,我将其与复制按钮组件一起使用,该组件接收我们的代码片段的代码。

要实现此功能,我们所需要做的就是向按钮添加单击按钮。并返回一个名为句柄复制的函数,其中包含以文本形式请求的代码。一旦被复制,那就是真的。我们可以显示不同的图标,表示复制成功。

从“react”导入React;
从“../svg/ClipboardIcon”导入剪贴板图标;
从“../svg/SuccessIcon”导入 SuccessIcon;
从“../utils/useCopyToClipboard”导入 useCopyToClipboard;

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

返回(
处理复制(代码)}>
{已复制? : }

);
}
如何添加重置间隔
我们可以对代码进行一项改进。正如我们目前编写的钩子一样,isCopied 将始终为 true,这意味着我们将始终看到成功图标:

成功-gif.gif
如果我们想在几秒钟后重置状态,您可以传递一个时间间隔来使用CopyToClipboard。让我们添加该功能。

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

然后我们将添加 useEffect 来表示,如果文本被复制并且我们有一个重置间隔,我们将在该间隔之后使用 setTimeout 将 isCopied 设置回 false。

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

从“react”导入React;
从“复制到剪贴板”导入副本;

导出默认函数useCopyToClipboard(resetInterval = null) {
const [isCopied, setCopied] = React.useState(false);

const handleCopy = React.useCallback((text) => {
if (typeof text === "string" || typeof text == "number") {
复制(text.toString());
setCopied(true);
} 其他 {
setCopied(假);
控制台.错误(
无法将 typeof ${typeof text} 复制到剪贴板,必须是字符串或数字。
);
}
}, []);

React.useEffect(() => {
让超时;
if (isCopied && 重置间隔) {
timeout = setTimeout(() => setCopied(false), resetInterval);
}
返回() => {
清除超时(超时);
};
}, [isCopied, ResetInterval]);

返回[isCopied,handleCopy];
}
最后,我们可以做的最后一个改进是将handleCopy包装在useCallback钩子中,以确保它不会在每次重新渲染时重新创建。

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

从“react”导入React;
从“../svg/ClipboardIcon”导入剪贴板图标;
从“../svg/SuccessIcon”导入 SuccessIcon;
从“../utils/useCopyToClipboard”导入 useCopyToClipboard;

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

返回(
处理复制(代码)}>
{已复制? : }

);
}
最终结果.gif

  1. usePageBottom 挂钩 在 React 应用程序中,有时了解用户何时滚动到页面底部非常重要。

在具有无限滚动功能的应用程序中,例如 Instagram,一旦用户点击页面底部,您就需要获取更多帖子。

Instagram 中的无限滚动
让我们看看如何为类似的用例(例如创建无限滚动)自己创建 usePageBottom 钩子。

我们首先在 utils 文件夹中创建一个单独的文件 usePageBottom.js,然后添加一个同名的函数(钩子):

// utils/usePageBottom.js
从“react”导入React;

导出默认函数 usePageBottom() {}

接下来,我们需要计算用户何时点击页面底部。我们可以通过窗口中的信息来确定这一点。为了访问它,我们需要确保调用钩子的组件已安装,因此我们将使用带有空依赖项数组的 useEffect 钩子。

// utils/usePageBottom.js
从“react”导入React;

导出默认函数usePageBottom() {
React.useEffect(() => {}, []);
}

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

// utils/usePageBottom.js
从“react”导入React;

导出默认函数usePageBottom() {
React.useEffect(() => {
window.innerHeight + document.documentElement.scrollTop ===
document.documentElement.offsetHeight;
}, []);
}

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

// utils/usePageBottom.js
从“react”导入React;

导出默认函数usePageBottom() {
const [bottom, setBottom] = React.useState(false);

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

返回底部;
}

但是,我们的代码无法正常工作。为什么不呢?

问题在于,每当用户滚动时我们都需要计算 isBottom。因此,我们需要使用 window.addEventListener 监听滚动事件。我们可以通过创建一个每当用户滚动时调用的本地函数来重新评估该表达式,该函数称为handleScroll。

// utils/usePageBottom.js
从“react”导入React;

导出默认函数usePageBottom() {
const [bottom, setBottom] = React.useState(false);

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

返回底部;
}

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

我们可以通过从 useEffect 和 window.removeEventListener 返回一个函数来实现这一点,我们在其中传递对同一 handleScroll 函数的引用。我们就完成了。

// utils/usePageBottom.js
从“react”导入React;

导出默认函数usePageBottom() {
const [bottom, setBottom] = React.useState(false);

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

返回底部;
}

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

在我的 Gatsby 网站中,我有一个标题,当我减小页面大小时,我想显示更少的链接。

调整窗口大小以显示标题
为此,我们可以使用媒体查询(CSS),或者我们可以使用自定义 React hook 来为我们提供页面的当前大小并隐藏或显示 JSX 中的链接。

之前,我使用的是一个名为react-use 的库中的钩子。我决定创建自己的挂钩来提供窗口的尺寸(宽度和高度),而不是引入整个第三方库。我将这个钩子称为 useWindowSize。

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

// utils/useWindowSize.js

从“react”导入React;

导出默认函数useWindowSize() {}

现在,由于我在服务器渲染的 Gatsby 站点中使用它,所以我需要获取窗口的大小。但我们可能无法访问它,因为我们在服务器上。

为了检查并确保我们不在服务器上,我们可以查看窗口类型是否不等于未定义的字符串。

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

// utils/useWindowSize.js

从“react”导入React;

导出默认函数useWindowSize() {
if (窗口类型!== "未定义") {
返回 { 宽度:1200,高度:800 };
}
}

如何获取窗口的宽度和高度
假设我们在客户端并且可以获得窗口,我们可以使用 useEffect 钩子通过与窗口交互来执行副作用。我们将包含一个空的依赖项数组,以确保仅在安装组件(调用此钩子)后才调用效果函数。

要找出窗口的宽度和高度,我们可以添加一个事件监听器并监听调整大小事件。每当浏览器大小发生变化时,我们都可以更新一个状态(使用 useState 创建),我们将其称为 windowSize,更新它的 setter 将是 setWindowSize。

// utils/useWindowSize.js

从“react”导入React;

导出默认函数useWindowSize() {
if (窗口类型!== "未定义") {
返回 { 宽度:1200,高度:800 };
}

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

React.useEffect(() => {
window.addEventListener("调整大小", () => {
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

从“react”导入React;

导出默认函数useWindowSize() {
// if (typeof window !== "undefined") {
// return { 宽度: 1200, 高度: 800 };
// }
const isSSR = typeof window !== "未定义";
const [windowSize, setWindowSize] = React.useState({
宽度: isSSR ? 1200:window.innerWidth,
高度:是SSR? 800:window.innerHeight,
});

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

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

如何删除调整大小事件监听器
您可以通过从 useEffectand 返回一个函数来做到这一点。我们将使用 window.removeEventListener 删除监听器。

// utils/useWindowSize.js

从“react”导入React;

导出默认函数useWindowSize() {
// if (typeof window !== "undefined") {
// return { 宽度: 1200, 高度: 800 };
// }
const isSSR = typeof window !== "未定义";
const [windowSize, setWindowSize] = React.useState({
宽度: isSSR ? 1200:window.innerWidth,
高度:是SSR? 800:window.innerHeight,
});

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

return () => {
  window.removeEventListener("resize", () => {
    setWindowSize({ width: window.innerWidth, height: window.innerHeight });
  });
};
登录后复制

}, []);
}

但是我们需要对同一函数的引用,而不是像这里那样的两个不同的函数。为此,我们将为两个侦听器创建一个名为changeWindowSize 的共享回调函数。

最后,在钩子结束时,我们将返回 windowSize 状态。就是这样。

// utils/useWindowSize.js

从“react”导入React;

导出默认函数useWindowSize() {
const isSSR = typeof window !== "未定义";
const [windowSize, setWindowSize] = React.useState({
宽度: isSSR ? 1200:window.innerWidth,
高度:是SSR? 800:window.innerHeight,
});

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

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

return () => {
  window.removeEventListener("resize", changeWindowSize);
};
登录后复制

}, []);

返回窗口大小;
}

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

就我而言,这是在 500 像素标记处。在那里,我想隐藏所有其他链接,只显示“立即加入”按钮,就像您在上面的示例中看到的那样:

//组件/StickyHeader.js

从“react”导入React;
从“../utils/useWindowSize”导入 useWindowSize;

函数 StickyHeader() {
const { width } = useWindowSize();

返回(


{/* 仅当窗口大于 500px /}
时可见 {宽度> 500 && (
<>

感言


价格



有问题吗?



)}
{/
在任何窗口大小下都可见 */}


立即加入



);
}

这个钩子适用于任何服务器渲染的 React 应用程序,例如 Gatsby 和 Next.js。

  1. useDeviceDetect 钩子 我正在为我的课程构建一个新的登陆页面,我在移动设备上遇到了一个非常奇怪的错误。在台式电脑上,样式看起来很棒。

但是当我在手机上查看时,一切都错位并且损坏了。

反应应用程序错误
我将问题追溯到一个名为“react-device-detect”的库,我用它来检测用户是否拥有移动设备。如果是这样,我会删除标题。

// templates/course.js
从“react”导入React;
从“react-device-detect”导入 { isMobile };

函数 Course() {
返回(
<>

{!isMobile && }
{/* 更多组件... */}

);
}

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

我如何创建 Hook
我在 utils 文件夹中为此挂钩创建了一个具有相同名称的单独文件 useDeviceDetect.js。由于钩子只是可共享的 JavaScript 函数,它利用了 React 钩子,因此我创建了一个名为 useDeviceDetect 的函数并导入了 React。

// utils/useDeviceDetect.js
从“react”导入React;

导出默认函数 useDeviceDetect() {}

如何从窗口获取用户代理
我们可以通过 userAgent 属性(位于 window 的 navigator 属性上)来确定是否可以获取有关用户设备的信息。

由于将窗口 API 作为 API/外部资源进行交互会被视为副作用,因此我们需要在 useEffect 挂钩中访问用户代理。

// utils/useDeviceDetect.js
从“react”导入React;

导出默认函数useDeviceDetect() {
React.useEffect(() => {
console.log(用户的设备是:${window.navigator.userAgent});
// 也可以写成 'navigator.userAgent'
}, []);
}

组件安装后,我们可以使用 typeof navigator 来确定我们是在客户端还是服务器上。如果我们在服务器上,我们将无法访问该窗口。 typeof navigator 将等于字符串 undefined,因为它不存在。否则,如果我们在客户端,我们将能够获取我们的用户代理属性。

我们可以使用三元组来表达这一切来获取 userAgent 数据:

// utils/useDeviceDetect.js
从“react”导入React;

导出默认函数useDeviceDetect() {
React.useEffect(() => {
const userAgent =
导航器类型===“未定义”? "" : navigator.userAgent;
}, []);
}

如何检查userAgent是否是移动设备
userAgent 是一个字符串值,如果使用移动设备,则该值将设置为以下任意一个设备名称:

Android、BlackBerry、iPhone、iPad、iPod、Opera Mini、IEMobile 或 WPDesktop。

我们所要做的就是获取我们得到的字符串,并使用 .match() 方法和正则表达式来查看它是否是这些字符串中的任何一个。我们将其存储在一个名为 mobile 的本地变量中。

我们将使用 useState 钩子将结果存储在状态中,我们将给它一个初始值 false。为此,我们将创建一个相应的状态变量 isMobile,并且 setter 将为 setMobile。

// utils/useDeviceDetect.js
从“react”导入React;

导出默认函数useDeviceDetect() {
const [isMobile, setMobile] = React.useState(false);

React.useEffect(() => {
const userAgent =
typeof window.navigator ===“未定义”? "" : navigator.userAgent;
const mobile = 布尔值(
userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
)
);
setMobile(移动设备);
}, []);
}

因此,一旦我们获得移动值,我们就会将其设置为状态。最后,我们将从钩子中返回一个对象,以便将来如果我们想选择向此钩子添加更多功能,则可以添加更多值。

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

// utils/useDeviceDetect.js
从“react”导入React;

导出默认函数useDeviceDetect() {
const [isMobile, setMobile] = React.useState(false);

React.useEffect(() => {
const userAgent =
typeof window.navigator ===“未定义”? "" : navigator.userAgent;
const mobile = 布尔值(
userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
)
);
setMobile(移动设备);
}, []);

返回 { isMobile };
}

最终结果
回到登陆页面,我们可以执行钩子并简单地从解构对象中获取该属性并在我们需要的地方使用它。

// templates/course.js
从“react”导入React;
从“../utils/useDeviceDetect”导入 useDeviceDetect;

函数 Course() {
const { isMobile } = useDeviceDetect();

返回(
<>

{!isMobile && }
{/* 更多组件... */}

);
}

结论
正如我试图通过每个示例来说明的那样,当第三方库无法满足要求时,自定义 React hook 可以为我们提供解决我们自己的问题的工具。

我希望本指南能让您更好地了解何时以及如何创建自己的 React hooks。请随意在您自己的项目中使用任何这些钩子和上述代码,并作为您自己的自定义 React 钩子的灵感。

以上是如何构建自己的 React Hook:分步指南的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!