希望是厄运的忠实的姐妹。——普希金
大家好,我是柒八九。
前言
在上一篇git 原理中我们在前置知识点中随口提到了Hook
。其中,就有我们比较熟悉的React Hook
。
而针对React Hook
而言,除了那些让人眼花缭乱的内置hook。其实,它最大的魅力还是自定义hook。
所以,今天我们就来讲几个,我们平时开发中可能会用到的自定义hook
。(文章内容可能有些长,请大家耐心观看,也可以先收藏后享用哦 😊)
当然,其实业界已经有很好的开源库,功能也强大的很多。(例如:ahooks)。但是它有一些让人诟病的问题,首先,有些功能其实我们在开发中不经常使用,并且引入了第三方库,反而使我们项目变得臃肿;其次,在开发中,我有一个比较执拗的做法,也就是别人的永远都是别人的。只有自己真正懂了,才是自己的。所以,大部分的工具库,我都选择手搓。(当然,也还没到了固执己见的地步,有些合适的库还是会用的)
所以,今天这篇文章,就给大家罗列一些在开发中,可能会用到并且能帮助到大家的自定义Hook
。
还有之前我们也有React
相关的文章,大家可以自行获取:
- React_Fiber机制(上)
- React_Fiber机制(下)
- React 元素 VS 组件
- React-全局状态管理的群魔乱舞
- 构建面向未来的前端架构
- React 18 如何提升应用性能
- React Server Components手把手教学
- React 并发原理
- 在React项目中使用CSS Module
- React Memo不是你优化的第一选择
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
React Hook
解析- React 自定义 Hook
1. 前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
React 内置Hook
以下是React
提供的一些标准内置Hooks
。你能相信,现在有15
个之多,如果大家有需要,到时候也可以写一篇关于内置hook的文章。
如果想看更详细的解释可以移步官网
2. React Hook 解析
追根溯源
在考虑使用Hooks
之前,首先要考虑原生JavaScript函数。
在
JavaScript
编程语言中,函数是可重用的代码逻辑,用于执行重复的任务。函数是可组合的,这意味着你可以在另一个函数中调用一个函数并使用其输出。
在下图中,someFunction()
函数组合(使用)了函数a()
和b()
。函数b()
使用了函数c()
。
毫无疑问,React
中的函数组件实际上就是普通的JavaScript
函数!因此,如果函数具有组合性,React组件也可以具有组合性。这意味着我们可以像下面的图像所示,将一个或多个组件组合(使用)到另一个组件中:
有状态组件 vs 无状态组件
在React
中,组件可以是有状态(stateful
)或无状态(stateless
)的。
- 一个有状态组件声明并管理本地状态。
- 一个无状态组件是一个纯函数,它没有本地状态和需要管理的副作用。
一个纯函数是一个没有副作用的函数。这意味着一个函数对于相同的输入始终返回相同的输出。
如果我们从函数组件中移除有状态和副作用逻辑,我们就得到了一个无状态组件。此外,有状态和副作用逻辑可以在应用程序的其他地方进行重复使用。因此,尽量将它们与组件隔离开来是有意义的。
React Hooks 和 有状态逻辑
通过React Hooks
,我们可以将状态逻辑
和副作用
从函数组件中隔离出来。
Hooks
是JavaScript函数,通过将它们与组件隔离开来来管理状态行为和副作用。
因此,现在我们可以将所有状态逻辑
隔离到Hooks中,并将它们用于组件中(因为Hooks本身也是函数,所以可以组合它们)。
状态逻辑
它可以是任何需要在本地声明和管理状态变量的内容。
例如,用于获取数据并将数据管理在本地变量中的逻辑是有状态的。我们可能还希望在多个组件中重复使用获取数据的逻辑。
以前,状态逻辑
只能在类组件中使用生命周期方法来实现。但是,有了React Hooks
,开发人员现在可以在函数组件中直接利用状态和其他React功能。
Hooks
提供了一种轻松地在多个组件之间重复使用有状态逻辑的方式,提高了代码的可重用性并减少了复杂性。它们使开发人员能够将复杂的组件拆分成更小、更易管理的部分,从而产生更清晰和更易维护的代码。
像useState
和useEffect
这样的Hooks
允许开发人员轻松地管理组件状态并处理副作用。由于其简单性和灵活性,React Hooks
已成为构建现代、高效和可扩展的React
应用程序的必备工具。
3. React 自定义 Hook
React自定义Hooks
是可重复使用的函数,允许开发人员以可重复使用的方式抽象和封装复杂的逻辑,用于共享非可视逻辑的Hooks模式
自定义
Hook
是通过组合现有的React Hooks
或其他自定义Hooks
来创建的。
它们允许开发人员从组件中提取通用逻辑,并在应用程序的不同部分之间共享它。自定义Hooks
遵循使用use
前缀的命名约定,这允许它们利用React
的Hooks
规则的优势。
通过创建自定义Hooks
,开发人员可以模块化和组织他们的代码,使其更易读、易维护和易测试。
这些Hooks
可以封装任何类型的逻辑,如API调用、表单处理、状态管理,甚至是抽象外部库。
我们采用Vite
构建一个React-TS
版本的项目。(yarn create vite my-vue-app --template react-ts
)
并且在src
文件下,新增hooks
文件夹,以存储下面我们定义的自定义hook
。然后我们通过配置alias
可以在组件中随意引入。即import xx from @hooks/xxx
前面我们讲过自定义Hooks是通过组合现有的React Hooks或其他自定义Hooks来创建的,所以下文中会有自定义hook的嵌套现象,大家在阅读的时候,需要甄别代码。(推荐大家还是自己弄一个小项目,自己实践一下)。
还有一点,由于篇幅所限,下面的hook
不做过多的解读。我们用了ts
,想必通过直接阅读代码,也能比较清晰的了解代码含义和限制。
3.1 useArray
import { useState, Dispatch, SetStateAction } from "react"; export type ArrayReturnType<T> { array: T[]; set: Dispatch<SetStateAction<T[]>>; push: (element: T) => void; filter: (callback: (value: T, index: number, array: T[]) => boolean) => void; update: (index: number, newElement: T) => void; remove: (index: number) => void; clear: () => void; } export default function useArray<T>(defaultValue: T[]): ArrayReturnType<T> { const [array, setArray] = useState<T[]>(defaultValue); function push(element: T) { setArray((a) => [...a, element]); } function filter(callback: (value: T, index: number, array: T[]) => boolean) { setArray((a) => a.filter(callback)); } function update(index: number, newElement: T) { setArray((a) => [ ...a.slice(0, index), newElement, ...a.slice(index + 1, a.length), ]); } function remove(index: number) { setArray((a) => [...a.slice(0, index), ...a.slice(index + 1, a.length)]); } function clear() { setArray([]); } return { array, set: setArray, push, filter, update, remove, clear }; }
useArray
hook利用React
的useState
hook来初始化和管理数组状态。它返回一个带有以下函数的对象:
push(element)
: 将指定的元素添加到数组中。filter(callback)
: 根据提供的回调函数对数组进行筛选,删除不满足条件的元素。update(index, newElement)
: 用newElement
替换指定索引处的元素。remove(index)
: 从数组中移除指定索引处的元素。clear()
: 清空数组,将其设置为空数组。
使用useArray
钩子,我们可以轻松地向数组中添加、更新、移除、筛选和清除元素,而无需处理复杂的逻辑。
import React from "react"; import useArray, { ArrayReturnType } from "@hooks/useArray"; // 在组件中使用(这里的使用方式不在赘述) const { array, set, push, remove, filter, update, clear }: ArrayReturnType<number> = useArray([ 1, 2, 3, 4, 5, 6, ]); // 在组件中定义回掉函数,处理相关逻辑
3.2 useAsync
import { useCallback, useEffect, useState } from "react"; export type AsyncReturn<T> = { loading: boolean; error?: Error | null; value?: T; }; export default function useAsync<T>( callback: () => Promise<T>, dependencies: unknown[] = [] ): AsyncReturn<T> { const [loading, setLoading] = useState(true); const [error, setError] = useState<Error>(); const [value, setValue] = useState<T | undefined>(); const callbackMemoized = useCallback(() => { setLoading(true); setError(undefined); setValue(undefined); callback() .then((result) => setValue(result)) .catch((err) => setError(err)) .finally(() => setLoading(false)); }, [...dependencies]); useEffect(() => { callbackMemoized(); }, [callbackMemoized]); return { loading, error, value }; }
u
seAsync钩子
接受一个执行异步操作的回调函数以及一个可选的依赖数组。它返回一个带有三个属性的对象:
loading
属性指示操作是否正在进行中error
属性保存在过程中遇到的任何错误消息value
属性包含异步操作的解析值
useAsync
使用useCallback
来记忆回调函数。这确保只有在依赖项发生变化时才会重新创建回调,防止不必要的重新渲染,并优化性能。此外,该钩子使用useState
和useEffect
钩子来管理加载状态,并在必要时调用记忆化的回调函数。
使用场景
无论我们是从API获取数据、执行计算还是处理表单提交,这个自定义钩子都简化了在React
组件中管理异步操作。
import React from "react"; import useAsync, { AsyncReturn } from "@hooks/useAsync"; export default function AsyncComponent() { const { loading, error, value }: AsyncReturn<string> = useAsync(() => { return new Promise<string>((resolve, reject) => { // 这里可以替换成正式场景 const success = false; setTimeout(() => { success ? resolve("成功了") : reject("失败了"); }, 1000); }); }); return ( <div> <div>Loading: {loading.toString()}</div> <div>{error}</div> <div>{value}</div> </div> ); }
3.3 useEventListener
import { RefObject, useEffect, useRef } from "react"; type EventCallback = (e: Event) => void; export default function useEventListener( eventType: string, callback: EventCallback, element: RefObject<HTMLElement> | EventTarget | null = window ) { const callbackRef = useRef<EventCallback | null>(null); useEffect(() => { callbackRef.current = callback; }, [callback]); useEffect(() => { if (element == null) return; if ( !(element instanceof EventTarget) && (element as RefObject<HTMLElement>).current == null ) return; const handler = (e: Event) => { if (callbackRef.current) { callbackRef.current(e); } }; if ((element as RefObject<HTMLElement>).current) { (element as RefObject<HTMLElement>).current?.addEventListener( eventType, handler ); } else { (element as EventTarget).addEventListener(eventType, handler); } return () => { if ((element as RefObject<HTMLElement>).current) { (element as RefObject<HTMLElement>).current?.removeEventListener( eventType, handler ); } else { (element as EventTarget).removeEventListener(eventType, handler); } }; }, [eventType, element]); }
使用useEventListener
我们可以指定事件类型
、回调函数
,甚至要附加事件侦听器的元素
(可以是ref
也可以是dom
)。这允许我们根据特定需求定制事件处理,提高了代码的可重用性。
该钩子还利用useRef钩子
来维护对回调函数的稳定引用。这确保了在组件的生命周期中即使回调函数发生变化,也使用最新版本的回调。这种动态行为使我们能够精确处理事件并响应应用程序状态的变化。
使用场景
useEventListener钩子
可以在各种情况下使用。无论我们需要捕获键盘事件
、监听滚动事件
或与用户输入交互
,这个钩子都可以胜任。
import { useState } from "react"; import useEventListener from "@hooks/useEventListener"; export default function EventListenerComponent() { const [key, setKey] = useState<string>(""); useEventListener("keydown", (e: Event) => { if (e instanceof KeyboardEvent) { setKey(e.key); } }); return <div> {key} </div>; }
上面示例中,useEventListener
利用这个钩子来跟踪用户按下的最后一个键。
3.4 useClickOutside
// 复用了上面的useEventListener钩子 import useEventListener from "@hooks/useEventListener"; import React from "react"; export default function useClickOutside( ref: React.RefObject<HTMLElement>, cb: (e: MouseEvent) => void, triggerRef?: React.RefObject<HTMLElement> ) { useEventListener( "click", (e) => { if ( ref.current == null || ref.current.contains(e.target as Node) || triggerRef.current?.contains(e.target as Node) ) return; cb(e as unknown as MouseEvent); }, document ); }
useClickOutside钩子
简化了检测点击事件是否发生在指定组件之外的过程。通过利用useEventListener钩子
,它在document级别
监听点击事件,允许我们在发生在提供的组件引用之外的点击时触发回调函数。
只需将钩子导入到我们的组件中,并传递所需组件的引用和回调函数,还有一个可选项-triggerRef
。
使用场景
useClickOutside
的潜在应用场景是无限的。在实现唤起弹窗
、下拉菜单
或任何在用户与其之外的任何元素交互时应该关闭的元素时,它特别有用。
下面示例中,我们特意将button
放置在Modal
之外,想必这也符合大家平时开发的模式。(所以,我们单独处理button
的点击,也就是需要有一个triggerRef
)。其实,我们完全可以将button
放置在modal
内部,做一个主动唤起的处理。(这在之前的文章中有介绍过,这里就不做展示了)
import { useRef, useState } from "react"; import useClickOutside from "@hooks/useClickOutside"; export default function ClickOutsideComponent() { const [open, setOpen] = useState<boolean>(false); const modalRef: React.RefObject<HTMLDivElement> = useRef(null); const triggerRef: React.RefObject<HTMLButtonElement> = useRef(null); useClickOutside( modalRef, () => { if (open) setOpen(false); }, triggerRef ); return ( <> <button onClick={() => setOpen(true)} ref={triggerRef}> 打开弹窗 </button> <div ref={modalRef} style={{ display: open ? "block" : "none", backgroundColor: "blue", color: "white", width: "100px", height: "100px", position: "absolute", top: "calc(50% - 50px)", left: "calc(50% - 50px)", }} > <span>我是一个萌萌哒的弹窗</span> </div> </> ); }
上面的情况,利用该钩子来切换弹窗的可见性。
- 点击
button
时候,弹窗开启,将open
状态设置为true
- 当用户在弹窗外点击(排除
button
)时,提供的回调函数将open
状态设置为false
,关闭窗口。