我写了一个 React hooks 库,名称叫做 @lilib/hooks,最近发布了 1.0.0,把它介绍给你们。
背景
我本意是想在 GitHub 上找一个好用的 hooks 库来帮助日常的开发,但是那么多库,没有一个满足心意。react-use 丰富,但异步数据请求的 hook 封装得太简单;react-query 专注于异步请求,但是 API 太复杂,文档也不直观;ahooks 是一个流行的库,但我实在难以接受其接口命名(例如与 useMount 对应的是 useUpdateEffect,而不是 useUpdate);剩余的库基本上都止步于丰富度太低。
优势
与其它库相比,并没有什么优势。@lilib/hooks 只能覆盖常见的开发场景;在处理网络请求时,并不会面面俱到;在接口命名方面,也是很主观的。
特点
- 支持 React 16.8+、17+ 和 18+。
- 支持服务端渲染。
- 支持 React 的严格模式(StrictMode)。
- 支持 TypeScript。
- 提供 50+ 钩子以覆盖常见开发业务。
- 提供全面的测试用例来保证代码质量。
安装
npm install --save @lilib/hooks
文档
场景
生命周期
import React from "react";
import { useMount, useUpdate, useUnmount } from "@lilib/hooks";
function Example(props) {
useMount(() => {
// 组件挂载
})
useUpdate(() => {
// 依赖更新
}, [props.someProp])
useUnmount(() => {
// 组件卸载
})
}
异步请求
处理异步请求,需要考虑到:相同请求去重、请求出错回退、重试、轮询、数据缓存、自动刷新、重新加载等。@lilib/hooks 提供了三个函数来处理异步请求:
useLoad:基础的异步请求钩子,它有大量的选项可供使用。useReload:重新加载useLoad的请求,无论它们是否在同一个组件中。useSubmit:useLoad的阉割版,仅仅提供一个便利用于增删改请求。
import React, { FormEvent } from "react";
import { useLoad, usePersist, useReload, useSubmit } from "@lilib/hooks";
function getUsers(): Promise<string[]> {
return fetch("/users").then((response) => {
return response.json();
});
}
function createUser(username: string) {
return fetch(`/users`, {
method: "POST",
body: JSON.stringify({ username }),
});
}
// 用户列表组件
function UserList() {
const { data, error, loading } = useLoad(
getUsers,
[],
{
// 请求 key,useReload 根据此 key 来重新加载请求。
// 此选项接受 non-nullable 数据,你甚至可以直接将 `getUsers` 函数作为 key。
key: "users",
// 初始数据
initialData: [],
// 开启轮询
polling: true,
// 轮询间隔为一分钟
pollingInterval: 60000,
}
);
if (error) {
return <span>出错了!</span>;
}
if (loading) {
return <span>加载中...</span>;
}
return (
<ul>
{data.map((username) => (
<li key={username}>{username}</li>
))}
</ul>
);
}
// 创建用户组件
function CreateUser() {
const reloadUsers = useReload("users");
const { submit, submitting } = useSubmit(
createUser,
{
// 新增成功的回调
onSuccess: () => {
alert("新增成功");
reloadUsers(); // 重新加载用户列表
},
// 新增失败的回调
onFailure: () => {
alert("新增失败");
},
}
);
// 处理提交事件,通过 `usePersist` 包裹的函数,其指针不会改变
const handleSubmit = usePersist((event: FormEvent) => {
event.preventDefault();
const input = document.getElementById("username") as HTMLInputElement;
submit(input.value); // 提交
});
return (
<form onSubmit={handleSubmit}>
<input id="username" placeholder="请输入用户名" />
<button type="submit">{submitting ? "提交中..." : "提交"}</button>
</form>
);
}
function App() {
return (
<>
<CreateUser />
<UserList />
</>
);
}
搜索节流
@lilib/hooks 提供了 useThrottle、useDebounce、useThrottledValue 和 useDebouncedValue 来节流和防抖。下面示例使用了 useThrottledValue 来实现搜索节流。
import React, { useState } from "react";
import { useThrottledValue, useLoad } from "@lilib/hooks";
function App() {
const [value, setValue] = useState("");
const [keyword] = useThrottledValue(value, { wait: 3000 }); // 节流处理
const { data, error, loading } = useLoad(
() => {
return fetch(`/search?keyword=${keyword}`).then((response) => {
return response.json();
}) as Promise<string[]>;
},
[keyword], // 重新请求的依赖数组
{
initialData: [],
}
);
return (
<div>
<input
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
placeholder="搜索..."
/>
{error ? (
"Error"
) : loading ? (
"Loading..."
) : (
<ul>
{data.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
</div>
);
}
组件封装
在封装函数组件时,常常需要将 ref 向下传递到真实的 DOM 元素上,如果组件内部也需要使用到 DOM,则可以通过 useComposedRef 来将它们合并。
import React, { forwardRef, ReactNode, useRef } from "react";
import { useComposedRef } from "@lilib/hooks";
interface ComponentProps {
children?: ReactNode;
}
const Component = forwardRef<HTMLDivElement, ComponentProps>((props, ref) => {
const domRef = useRef(null);
const composedRef = useComposedRef(domRef, ref);
// Do something.
return <div ref={composedRef}>{props.children}</div>;
});
执行动画
执行 JavaScript 通常会使用 requestAnimationFrame 来提升性能。useAnimation 钩子在回调函数中直接将时间转化为了百分比,方便你使用,且在组件卸载时会自动调用 cancelAnimationFrame,无需手动处理。
import React, { useRef } from "react";
import TweenJS from "@tweenjs/tween.js";
import { useAnimation } from "@lilib/hooks";
function Example() {
const domRef = useRef<HTMLDivElement>(null);
const [start, cancel] = useAnimation(
(percent) => {
// percent 是一个介于 0 到 1 之间的小数
if (domRef.current) {
domRef.current.style.width = percent * 100 + "%";
}
},
{
duration: 1000,
algorithm: TweenJS.Easing.Bounce.Out,
}
);
return (
<>
<div>
<button onClick={start}>Start</button>{" "}
<button onClick={cancel}>Cancel</button>
</div>
<div
ref={domRef}
style={{ width: 0, height: 8, marginTop: 8, backgroundColor: "orange" }}
></div>
</>
);
}
功能探测
import React, { useRef } from "react";
import {
useDarkMode,
usePageVisible,
useWindowFocus,
useIntersecting,
} from "@lilib/hooks";
function Example() {
// 是否为暗黑模式
const isDarkMode = useDarkMode();
// 页面是否可见
const isPageVisible = usePageVisible();
// 窗口是否聚焦
const isWindowFocus = useWindowFocus();
const rootRef = useRef(null);
const targetRef = useRef(null);
// 判断两个元素之间是否相交
const isIntersecting = useIntersecting(targetRef, { root: rootRef });
return (
<div ref={rootRef} style={...}>
<div ref={targetRef} style={...}>Target</div>
</div>
);
}
总结
上面的场景仅占 @lilib/hooks 的一小部分,更多详情请查看文档。