文章目录
目录导航
快速定位文章内容
正文
刚开始接触 React 的时候,我有一段时间特别容易被一句话影响:
“重复两次就要抽。”
所以只要我看到有一点点相似逻辑,就会开始想:
- 要不要提函数
- 要不要提组件
- 要不要干脆提成一个自定义 hook
当时会觉得这是一种“更高级”的写法。
代码看起来也确实更像做过整理。
但后来我慢慢发现,很多项目不是因为“没抽 hook”而变乱,
反而是因为“抽得太早、抽得太快、抽得太像模板”,最后把原本还算直白的逻辑包得越来越难读。
所以我现在对 hook 的态度,和以前不太一样了。
不是不能抽。
而是先判断:
我抽出来之后,到底是让代码更清楚了,还是只是让它看起来更像复用了?
我现在不太会为了“形式上的复用”去抽 hook
有一种情况我现在特别警惕。
就是两个地方看起来“好像差不多”,于是很快就抽出一个
useSomething。比如:
- 都有
open/close - 都有
loading - 都有一个输入值
- 都有一个列表请求
表面看很像。
但一旦往里加两三个真实需求,就会发现:
它们其实只是在名字上相似,处理细节完全不同。
最后你就会得到一种很尴尬的东西:
- 参数越来越多
- 返回值越来越多
- 分支越来越多
- 用的时候还得回头翻 hook 里到底帮你做了什么
这类 hook 往往不是在降低复杂度,而是在转移复杂度。
我真正会抽 hook 的前提,是“这段逻辑已经形成稳定模式了”
比如下面这些情况,我现在比较愿意抽。
1. 一个交互模式已经在多个地方反复出现
比如弹窗开关:
const [open, setOpen] = useState(false) const show = () => setOpen(true) const hide = () => setOpen(false)
如果项目里很多地方都这么用,而且使用方式很一致,那提一个
useDisclosure 就比较自然。因为它不是抽象未来。
而是在总结已经稳定下来的模式。
2. 一段逻辑本身就有独立语义
比如:
useSearchParamsStateuseCopyToClipboarduseDebouncedValueusePaginationuseMounted
这类 hook 即使单独拿出来,也能说清自己在干什么。
我很在意这一点。
因为一个 hook 如果名字起出来,还是说不明白职责,那它大概率也没有真的抽清楚。
3. 抽出来之后,调用方会明显更容易读
比如页面里原本有 40 行滚动监听、清理副作用、状态切换逻辑,
抽成
useStickyHeader() 之后,页面主体代码明显干净了,
而且阅读页面的人并不需要先理解内部细节才能继续往下读。这种抽法通常是值得的。
哪些情况我现在反而不急着抽
1. 逻辑只在一个地方出现,而且还在变化
这类我现在很少一上来就抽。
因为它还没稳定。
今天要这个,明天又要那个。
你现在抽出来,过两天大概率还得拆回去。
还不如先老老实实放在页面里长一阵,等模式清楚了再收。
2. 只是几行简单状态
比如:
const [activeTab, setActiveTab] = useState("all")
这种我通常不会为了“代码看起来更抽象”去提一个
useTabState()。因为它本来就已经够清楚了。
抽了反而会多一层跳转成本。
3. 两段逻辑只是外形相似,业务语义不同
比如两个列表页都有:
- 请求数据
- loading
- 错误处理
但一个是搜索驱动,一个是分页驱动,一个带缓存,一个带筛选联动。
这种我通常不会急着合并成一个“大而全”的请求 hook。
因为最后你很可能会得到一个谁都能用一点、谁都用得不顺手的东西。
我现在更想避免的是“抽象抢跑”
这是我后来越来越在意的一件事。
有些抽象不是因为代码真的成熟了,而是因为人先焦虑了。
总觉得:
- 这样是不是不够优雅
- 这样是不是不够通用
- 这样是不是后面不好复用
于是很早就开始抽。
可问题是,很多时候你根本还不知道“未来真正会怎么复用”。
这时做出来的抽象,往往只是对未来的一种猜测。
而过早猜测,通常比暂时重复更容易出错。
因为重复顶多只是多写几行。
抽错了,却会把整段逻辑的理解成本一起抬高。
我现在判断 hook 值不值得抽,通常会看三件事
1. 它是不是已经稳定了
不是“我猜以后会重复”,而是“它现在已经在多个地方以相似方式出现”。
2. 它有没有独立语义
一个好的 hook,不该只是把若干
useState 和 useEffect 打包起来。它最好能回答一句话:
这个 hook 负责什么?
如果这句话说不顺,那通常还不够成熟。
3. 抽完之后,调用方是不是更清楚了
这是最关键的。
如果抽完之后,页面代码确实更容易看,职责也更明确,那这次抽象通常有价值。
如果抽完之后只是把复杂度藏到了另一个文件里,
那这不叫简化,只叫转移。
一个我现在比较认可的例子
比如复制按钮逻辑:
"use client" import { useState } from "react" export function useCopyText() { const [copied, setCopied] = useState(false) const copy = async (text: string) => { await navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => { setCopied(false) }, 1500) } return { copied, copy } }
这个 hook 我会觉得还比较自然。
因为它有明确职责:
- 负责复制文本
- 管理复制成功后的短暂状态反馈
调用方也会更干净:
const { copied, copy } = useCopyText()
这时候 hook 是在提供一个完整的小能力,而不是单纯把几行状态搬家。
一个我现在会谨慎的例子
比如有人很容易写出这种东西:
usePageState()
然后里面塞:
- loading
- modal 开关
- 当前选中项
- 搜索词
- 排序
- 分页
- 请求逻辑
- reset 方法
这种 hook 看起来很省事,但时间一长通常会越来越重。
因为它的职责边界太宽了。
最后页面不是更简单,而是更依赖这个黑盒。
一旦你要改其中一段逻辑,就很容易牵一发动全身。
我现在更喜欢“小而明确”的 hook
如果真的要抽,我通常更偏向这类方向:
useDisclosureuseDebouncedValueuseCopyTextuseInfiniteScrolluseQueryStateuseIsMounted
这些 hook 有一个共同点:
- 解决的问题单一
- 名字能说明职责
- 调用方读起来直观
- 不会轻易绑死某一个页面的全部业务
这种抽法后面通常更稳。
最后
我现在不会再把“抽成 hook”自动等同于“代码更高级”。
因为真正重要的不是有没有抽,而是抽完之后,代码有没有更清楚。
如果一段逻辑还在长、还在变、还没稳定,
那暂时放在原地,很多时候比急着抽更健康。
如果它已经在多个地方形成稳定模式,而且抽出来之后能明确表达一个能力,
那 hook 就会非常好用。
如果一句话总结我现在的习惯,就是:
不是为了复用而抽,而是等模式长出来之后,再把它收成一个清楚的能力。