最近在接收别人的项目,装依赖的时候我瞟了一眼 package.json,ahooks、@mantine/hooks、react-use 三个都在。这不稀奇,很多人就这么拼。稀奇的是我写 useMouse 的时候卡了几秒,得先想清楚从哪个库 import。
三个库加起来两百多个 Hook,最常用的那几个签名却各写各的。有的返回元组,有的返回对象;有的让你传 ref,有的让你传元素;这个库叫 useLocalStorage,隔壁叫 useLocalStorageState。
后来有人给我推了 reause(其实我就是作者,笑),说这是一站式。我没直接信,把源码拉下来数了一遍。这篇写的就是数出来的结果。

- 文档地址:reause.netlify.app/
- 仓库地址:github.com/hairyf/reax...
一、先把账算清楚
一个 Hook 库有多少 Hook,最直接的办法是数 packages/<包>/<名字>/index.tsx 下面有多少个目录。reause 是 286。
放到同行里,差距就出来了:
| 库 | 数量 | 口径 |
|---|---|---|
| reause | 286 个目录(use* 命名 263 个) |
packages/*/*/index.tsx |
| react-use | 112 | src/ 下以 use 开头的源文件 |
@mantine/hooks |
81 个目录,index.ts 136 条导出 |
src/use-*/ |
| ahooks | 74 | src/ 下带 index.ts 的目录 |
| react-hookz/web | 58 个目录,index.ts 60 条导出 |
src/ 下除 util 的目录 |
按包拆开,重心更清楚:
text
core 155 个 30573 行
shared 84 个 6426 行
math 19 个 483 行
integrations 12 个 2510 行
rxjs 7 个 646 行
electron 6 个 473 行
firebase 3 个 394 行
286 个目录的实现合计 41505 行,跟着 Hook 走的 index.md 有 284 个,*.test.tsx 也是 284 个,demo.tsx 280 个。缺的那几个都是内部工具目录。文档和测试基本一比一贴着实现走。
二、286 个 Hook 都在干什么
数量只是入场券,能顶掉你现有依赖才算数。我按场景过了一遍。
浏览器和 DOM 这档最完整。useMouse、useScroll、useElementSize、useResizeObserver、useIntersectionObserver、useMediaQuery、usePreferredDark、useClipboard、useTextareaAutosize、useVirtualList,VueUse 里那批浏览器 API 基本原样搬过来了。
存储有三件。useStorage 用一份实现包了 localStorage 和 sessionStorage,序列化器自动推断,Map、Set、Date、对象、数字各走各的;useLocalStorage 是它的语法糖;useIDBKeyval 走 IndexedDB。
交互和拖拽有 useDraggable、useSplitter、useHotkeys、useClickAway、useFocusTrap、useSortable、useFocusReturn。
网络和数据有 useFetch、useAxios、useWebSocket、useFuse、useAsyncValidator、useJwt。
时间这块有 useTimeAgo、useIdle、useInterval、useTimeout,还有带历史记录的 useStateDebouncedHistory 和 useStateThrottledHistory,拿来做撤销重做省不少事。
差别在生态包。ahooks 是一棵大树塞进一个包,reause 拆成了四个:@reause/rxjs 7 个、@reause/electron 6 个、@reause/firebase 3 个、@reause/integrations 12 个。最后那个把 axios、qrcode、fuse.js、Sortable、Drauu、NProgress 全包了。
拆分本身不算优点,但有个实际好处:不装 @reause/firebase,firebase 就不进你的依赖树。@reause/integrations 的 peerDependencies 里躺着 axios、qrcode、sortablejs、jwt-decode 十来个库,全都按需装。
来源上它不是只搬 VueUse 一遍。meta/functions.md 按来源拆是这样:
text
vueuse 298
react-use 20
mantine 12
ahooks 9
react-hookz 2
reause 独有 10
除了 VueUse,又从 react-use、mantine、ahooks 各补了一部分,最后自己加了 10 个上游没有的。状态列里 337 个标 ✅ ported,10 个标 ✅ reause-only export,4 个标 ✅ ported (not in pinned submodule)。
也就是说,如果你现在装了好几个 Hook 库,reause 基本都能全部替换。顶不掉的通常是 ahooks 里 useAntdTable、useFusionTable 那种跟具体 UI 库绑死的,以及业务里自己攒的私有 Hook。
三、高质量的开发参考文档
我一开始是冲着 Hook 数量去看的,最后被文档站留下了。
每个 Hook 的文档页都不是手写的。构建期会往页面里注入函数信息块(Category、Export Size、Package、Last Changed)、Demo、类型声明、源码链接、贡献者和 Changelog。index.md 里只写正文,其余全套是生成的。
Demo 是真的能跑的组件,不是静态截图代码块。useMouse 页面上,鼠标在页面里动,下面的 JSON 就跟着变:

代码块里的类型是构建期用 twoslash 跑出来的。鼠标悬在函数名上,签名和重载会直接弹出来,下面还带着上游出处:

类型声明区也是从这个 Hook 的源模块里抽的,跟实现同一个来源,不会写跑偏:

这三件事拼起来其实就是 vueuse 的「自动化文档」:Demo 可交互,类型可悬停,声明从源码抽,并且在 skills 生成中非常有优势。
四、如何从 VueUse 搬迁
VueUse 是 Vue 的,搬到 React 不是翻译代码就完事,名字和返回值先得对。
仓库的 AGENTS.md 定了两条路:Vue 体系过来的要转换适配,React 体系(react-use 这类)直接镜像。转换的规则是三条:
text
ref* -> useState*
on* -> use*
use*RefHistory -> useState*History
落到具体文件上,就是 useRefHistory 变 useStateHistory,onClickOutside 变 useClickOutside,refDebounced 变 useStateDebounced。注册表里 213 条 VueUse 映射,重新设计的有四十一个。
目录结构一比一:packages/<包>/<函数>/ 下面固定 index.tsx、index.md、demo.tsx、index.test.tsx,跟 VueUse 的相对路径对齐。唯一例外是 @reause/metadata,它是生成物。
测试绝不马虎,全包包含所有上游的测试,并都是从上游继承,全量测试通过。
五、VueUse 之外,还额外收录了四个上游 Hook Lib
搬 VueUse 只解决了浏览器 API 这一半。还有更多react 特性的 hooks 库,其他 react hooks 做得更好。
reause 的做法是把它们加进已有的包,而不是一源一包。函数名跟上游保持一致的直接镜像,参数类型按上面那套收窄。每处移植都在 JSDoc 里记了出处:
ts
// packages/core/useMask/index.tsx:2
* Map from @mantine/hooks `useMask`
// packages/core/useClickAway/index.tsx:56
* Map from ahooks `useClickAway`
// packages/shared/createGlobalState/index.tsx:27
* Map from react-use `createGlobalState` (source/react-use/src/factory/createGlobalState.ts).
全仓库这样的 Map from 有 297 处。按 meta/functions.md 的统计,react-use 进来 20 个、mantine 12 个、ahooks 9 个、react-hookz 2 个。
文档站的 Source 链接是照着这张出处表生成的,所以 react-use 来的页面标的是 react-use,不会一律标成 VueUse:
text
Source · Demo · react-hookz
对我这种要写进 commit message 的人来说,出处写清楚比多搬十个 Hook 有用,至少出了问题知道去哪个仓库翻。
六、286 个 Hook 不各长各的,靠机器管
Hook 数量多本身不算难事,难的是塞进去之后 API 还能保持一致。混用多个库的真实成本不在体积,在你的肌肉记忆得记两套签名。
返回值有约定:两个以上可写值返回对象加配对 setter,恰好一个可写值返回元组。packages/shared/useCounter/index.tsx:98 返回 { count, inc, dec, get, set, reset },packages/rxjs/useObservable/index.tsx:27 声明 [value: H | I, setValue: Dispatch<SetStateAction<H | I>>]。规则一句话讲完,286 个 Hook 都守着这条线。
规矩不写在文档里,写在 tsc 判得死的类型里,还配了类型测试。全仓库 expectTypeOf 断言有 274 处。比如「DOM 目标不收纯元素」这条,断言原文是这样:
ts
// packages/core/useResizeObserver/index.test.tsx:74
// 纯元素被刻意拒绝,ref 是唯一的 DOM 目标形式
expectTypeOf<HTMLElement>()
.not
.toMatchTypeOf<ElementTargetOrArray<HTMLElement>>()
AGENTS.md 里那条质量底线原文是「CI 必须 100% 绿(允许 flaky 测试重跑一次)」。它有配套:一次提交只改一个 Hook,在独立 worktree 里干活,元数据文件只有一个写者,本地三连跑 vitest、eslint、tsc --noEmit。
七、AI 在这里是干什么的
286 个 Hook 要跟上上游,人肉肯定是跟不动的了,光对差异就是个体力活。
reause 把同步交给了一条子代理流水线:轮询上游、找差异、开 Issue、派子代理实现、提 PR、跑 CI、合并、每晚发版。每个子代理只在一个独立 worktree 里改一个 Hook,PR 必须带 Closes #<N>。
仓库还发布了一个 @reause/skills 包,就像 vueuse 一样,装上之后 AI 助手能查这套 Hook 的正确用法:
bash
npx skills add hairyf/reause
让会自己它用 useLocalStorage 存数据、useTitle 改标题、useClipboard 做复制,出来的 import 路径和参数形态都是对的,没编出不存在的 Hook。这是给 286 个 Hook 配的说明书,顺带也让 AI 写这个库的代码不容易跑偏。
八、明确迁移的 27 个 hooks
meta/functions.md 里 vueuse 来源有 298 行,但并不是全搬。27 个 VueUse 函数被明确判死不搬,core 5 个、shared 22 个,里面有 toRef、toRefs、reactify、tryOnMounted、computedWithControl。
最典型的是 computedWithControl。仓库里第 14 号议题专门讨论过,维护者的结语是这个 API 依赖 Vue 的 computed/effect 依赖追踪系统,React 里无法等价实现,因为 React 的依赖靠 props/state 显式传递。这话没毛病,硬搬只能得到一个语义被阉割的仿制品。
这 27 个挂在 impractical 标签下,议题全关了,审计文档里写死一句:已判定不适用于 React,不得再派发。
九、一些结论
reause 现在是 0.1.8,还在 0.x,API 有变动的可能,上生产之前锁版本。
每个包只产出单个 dist/index.d.ts,exports 里写的是 "./*": "./dist/*",所以 @reause/core/useMouse 这种深路径导入是不通的,类型和文档都停在包级别。好处是外部不用操心导出结构,坏处是文档构建时任何 barrel import 都会把整包类型图拉进 TS program,于是有了上面那套按块注入。这个坑你大概率用不到,但它说明项目还有工程债。
「最全」这个结论我有明确口径:按单个库能提供的 Hook 数量比,263 对 112,reause 赢得很干脆;按 API 一致性比,它把值参数和 ref 参数彻底分家,比多库混用省心。但它没到能替掉一切的程度,像 @tanstack/react-query 那类带缓存和失效策略的数据层,它压根没打算碰。
如果你手上也有装了三个 Hook 库的项目,建议花半小时翻一遍它的包列表,看有多少能对上。仓库在 github.com/hairyf/reau...。哪个 Hook 你踩过坑,或者觉得哪个库被低估了,评论区聊。