React 自实现 Hooks

引言

我在这里记录了自己实现了一些常用 Hooks, 包括小程序框架这种, 用来提高日常的开发速度.

单独在这里记录是因为会使用到 Taro, 直接实现包可能导致无法正常使用; 如果需要, 你可以直接复制对应 Hook 的源代码进行使用.


如果可以的话, 希望你可以保留对应的作者注释, 即便我看不到, 我也会很开心 😸

通用

useAsyncEffect

允许您直接在 useEffect 中, 直接使用 async/await 语法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { useEffect, useRef } from "react";
import type { DependencyList } from "react";

/**
* @name AsyncEffectCleanup
* @description 异步 effect 返回的清理函数类型
*/
type AsyncEffectCleanup = () => void;

/**
* @name AsyncEffectCallback
* @description useAsyncEffect 的回调函数类型
* 它可以返回一个 Promise,该 Promise resolve 为一个清理函数,
* 或者 resolve 为 void。
*/
type AsyncEffectCallback = () => Promise<AsyncEffectCleanup | void>;

/**
* 一个健壮的 useAsyncEffect Hook,允许 effect 异步执行,
* 并且不需要调用者手动使用 useCallback。
*
* @param {AsyncEffectCallback} asyncEffect
* 一个异步函数。它可以选择性地返回一个 Promise,
* 该 Promise resolve 为一个“清理函数”。
*
* @param {DependencyList} [deps]
* 依赖项数组。可选,行为与 useEffect 一致。
*
* @author kaedeshimizu
* @email kaedeshimizu@qq.com
*/
const useAsyncEffect = (
asyncEffect: AsyncEffectCallback,
deps: DependencyList,
): void => {
// 使用 ref 存储最新的 effect 回调
const effectRef = useRef<AsyncEffectCallback>(asyncEffect);

// 每次渲染时,更新 ref 的值
effectRef.current = asyncEffect;

useEffect(() => {
let isMounted = true;

// 明确定义清理函数的类型
let cleanup: AsyncEffectCleanup = () => {};

const execute = async () => {
try {
// 从 ref 中调用
// 'result' 的类型被 TS 推断为 `AsyncEffectCleanup | void`
const result = await effectRef.current();

if (isMounted && typeof result === "function") {
cleanup = result;
} else if (!isMounted && typeof result === "function") {
// 如果在 resolve 之前就卸载了,立即执行清理
result();
}
} catch (e) {
// 在组件挂载时才抛出错误,避免内存泄漏
if (isMounted) {
console.error("useAsyncEffect 出现错误:", e);
}
}
};

// 执行
execute();

// 主清理
return () => {
isMounted = false;
// 在卸载时执行 effect 返回的清理函数
cleanup();
};

// 依赖项
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};

export default useAsyncEffect;

Taro

测试 Taro 版本: ^4.1.6

useLayoutHeight

动态的, 获取某些指定元素的高度, 单位为 px.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import { useState, useLayoutEffect } from "react";
import Taro from "@tarojs/taro";

type ElementRect = { height: number } | null;
type QueryResult = ElementRect[] | null;

/**
* 获取多个选择器对应的所有元素的总高度
* @param selectors 选择器列表
* @param dependences 依赖列表, 变化后会自动重新计算高度
* @author kaedeshimizu
* @email kaedeshimizu@qq.com
*/
const useLayoutHeight = (selectors: string[], dependences: any[] = []) => {
const [height, setHeight] = useState(0);

const selectorsKey = JSON.stringify(selectors);

useLayoutEffect(() => {
let isMounted = true;

if (!selectors || selectors.length === 0) {
setHeight(0);
return;
}

try {
const query = Taro.createSelectorQuery();

selectors.forEach((sel) => {
query.selectAll(sel).boundingClientRect();
});

query.exec((res: QueryResult[]) => {
if (!isMounted) return;

// 你的 reduce 逻辑非常健壮, 很好地处理了 null
const totalHeight = res.reduce((acc, rects) => {
const groupHeight =
rects?.reduce((groupAcc, rect) => {
return groupAcc + (rect?.height || 0);
}, 0) || 0;

return acc + groupHeight;
}, 0);

setHeight(totalHeight);
});

return () => {
isMounted = false;
};
} catch (err) {
console.error("useLayoutHeight 同步错误:", err);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectorsKey, ...dependences]);

return height;
};

export default useLayoutHeight;

useNavInfo

获取顶部安全区的一些相关信息, 包括微信小程序胶囊的一些信息, 以及屏幕的一些信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { useState, useEffect } from "react";
import Taro from "@tarojs/taro";

interface INavInfo {
statusBarHeight: number;
titleBarHeight: number;
titleBarWidth: number;
appHeaderHeight: number;
marginSides: number;
capsuleWidth: number;
capsuleHeight: number;
capsuleLeft: number;
contentHeight: number;
screenHeight: number;
windowHeight: number;
}

/**
* useNavInfo
* @description 用来获取顶部高度, 宽度相关操作的钩子
* @author kaedeshimizu
* @email kaedeshimizu@qq.com
* @example
* const navInfo = useNavInfo();
* return (
* <View
* className="background-card"
* style={{
* marginTop: navInfo.appHeaderHeight + "px",
* }}
* ></View>
* )
*/
const useNavInfo = (): INavInfo => {
const [navInfo, setNavInfo] = useState({
statusBarHeight: 0,
titleBarHeight: 0,
titleBarWidth: 0,
appHeaderHeight: 0,
marginSides: 0,
capsuleWidth: 0,
capsuleHeight: 0,
capsuleLeft: 0,
contentHeight: 0,
screenHeight: 0,
windowHeight: 0,
});

useEffect(() => {
// 判断当前环境是否可用, 不可用则直接返回0
const { statusBarHeight, screenWidth, screenHeight, windowHeight } =
Taro.getEnv() === "WEAPP" || Taro.getEnv() === "HARMONYHYBRID"
? Taro.getWindowInfo()
: {
statusBarHeight: 0,
screenWidth: 0,
screenHeight: 0,
windowHeight: 0,
};

// 获取胶囊信息
const { width, height, left, top, right } =
Taro.getEnv() === "WEAPP" ||
Taro.getEnv() === "TT" ||
Taro.getEnv() === "HARMONYHYBRID"
? Taro.getMenuButtonBoundingClientRect()
: { width: 0, height: 0, left: 0, top: 0, right: 0 };
// 计算标题栏高度
const titleBarHeight = height + (top - statusBarHeight!) * 2;
// 计算导航栏高度
const appHeaderHeight = statusBarHeight! + titleBarHeight;
//边距,两边的
const marginSides = screenWidth - right;
//标题宽度
const titelBarWidth = screenWidth - width - marginSides * 3;
//去掉导航栏,屏幕剩余的高度
const contentHeight = screenHeight - appHeaderHeight;

setNavInfo({
statusBarHeight: statusBarHeight || 0, //状态栏高度
titleBarHeight: titleBarHeight, //标题栏高度
titleBarWidth: titelBarWidth, //标题栏宽度
appHeaderHeight: appHeaderHeight, //整个导航栏高度
marginSides: marginSides, //侧边距
capsuleWidth: width, //胶囊宽度
capsuleHeight: height, //胶囊高度
capsuleLeft: left,
contentHeight: contentHeight,
screenHeight: screenHeight,
windowHeight: windowHeight,
});
}, []);

return navInfo;
};

export default useNavInfo;