文章

React

在 React 里,什么时候我会把逻辑抽成 hook,什么时候不会

发布于2026-05-09更新于2026-05-09阅读时间8 分钟
文章目录
正文
刚开始接触 React 的时候,我有一段时间特别容易被一句话影响:
“重复两次就要抽。”
所以只要我看到有一点点相似逻辑,就会开始想:
  • 要不要提函数
  • 要不要提组件
  • 要不要干脆提成一个自定义 hook
当时会觉得这是一种“更高级”的写法。 代码看起来也确实更像做过整理。
但后来我慢慢发现,很多项目不是因为“没抽 hook”而变乱, 反而是因为“抽得太早、抽得太快、抽得太像模板”,最后把原本还算直白的逻辑包得越来越难读。
所以我现在对 hook 的态度,和以前不太一样了。
不是不能抽。 而是先判断:
我抽出来之后,到底是让代码更清楚了,还是只是让它看起来更像复用了?

我现在不太会为了“形式上的复用”去抽 hook

有一种情况我现在特别警惕。
就是两个地方看起来“好像差不多”,于是很快就抽出一个 useSomething
比如:
  • 都有 open / close
  • 都有 loading
  • 都有一个输入值
  • 都有一个列表请求
表面看很像。 但一旦往里加两三个真实需求,就会发现:
它们其实只是在名字上相似,处理细节完全不同。
最后你就会得到一种很尴尬的东西:
  • 参数越来越多
  • 返回值越来越多
  • 分支越来越多
  • 用的时候还得回头翻 hook 里到底帮你做了什么
这类 hook 往往不是在降低复杂度,而是在转移复杂度。

我真正会抽 hook 的前提,是“这段逻辑已经形成稳定模式了”

比如下面这些情况,我现在比较愿意抽。

1. 一个交互模式已经在多个地方反复出现

比如弹窗开关:
tsx
const [open, setOpen] = useState(false) const show = () => setOpen(true) const hide = () => setOpen(false)
如果项目里很多地方都这么用,而且使用方式很一致,那提一个 useDisclosure 就比较自然。
因为它不是抽象未来。 而是在总结已经稳定下来的模式。

2. 一段逻辑本身就有独立语义

比如:
  • useSearchParamsState
  • useCopyToClipboard
  • useDebouncedValue
  • usePagination
  • useMounted
这类 hook 即使单独拿出来,也能说清自己在干什么。
我很在意这一点。
因为一个 hook 如果名字起出来,还是说不明白职责,那它大概率也没有真的抽清楚。

3. 抽出来之后,调用方会明显更容易读

比如页面里原本有 40 行滚动监听、清理副作用、状态切换逻辑, 抽成 useStickyHeader() 之后,页面主体代码明显干净了, 而且阅读页面的人并不需要先理解内部细节才能继续往下读。
这种抽法通常是值得的。

哪些情况我现在反而不急着抽

1. 逻辑只在一个地方出现,而且还在变化

这类我现在很少一上来就抽。
因为它还没稳定。 今天要这个,明天又要那个。 你现在抽出来,过两天大概率还得拆回去。
还不如先老老实实放在页面里长一阵,等模式清楚了再收。

2. 只是几行简单状态

比如:
tsx
const [activeTab, setActiveTab] = useState("all")
这种我通常不会为了“代码看起来更抽象”去提一个 useTabState()
因为它本来就已经够清楚了。
抽了反而会多一层跳转成本。

3. 两段逻辑只是外形相似,业务语义不同

比如两个列表页都有:
  • 请求数据
  • loading
  • 错误处理
但一个是搜索驱动,一个是分页驱动,一个带缓存,一个带筛选联动。
这种我通常不会急着合并成一个“大而全”的请求 hook。
因为最后你很可能会得到一个谁都能用一点、谁都用得不顺手的东西。

我现在更想避免的是“抽象抢跑”

这是我后来越来越在意的一件事。
有些抽象不是因为代码真的成熟了,而是因为人先焦虑了。
总觉得:
  • 这样是不是不够优雅
  • 这样是不是不够通用
  • 这样是不是后面不好复用
于是很早就开始抽。
可问题是,很多时候你根本还不知道“未来真正会怎么复用”。
这时做出来的抽象,往往只是对未来的一种猜测。
而过早猜测,通常比暂时重复更容易出错。
因为重复顶多只是多写几行。 抽错了,却会把整段逻辑的理解成本一起抬高。

我现在判断 hook 值不值得抽,通常会看三件事

1. 它是不是已经稳定了

不是“我猜以后会重复”,而是“它现在已经在多个地方以相似方式出现”。

2. 它有没有独立语义

一个好的 hook,不该只是把若干 useStateuseEffect 打包起来。
它最好能回答一句话:
这个 hook 负责什么?
如果这句话说不顺,那通常还不够成熟。

3. 抽完之后,调用方是不是更清楚了

这是最关键的。
如果抽完之后,页面代码确实更容易看,职责也更明确,那这次抽象通常有价值。
如果抽完之后只是把复杂度藏到了另一个文件里, 那这不叫简化,只叫转移。

一个我现在比较认可的例子

比如复制按钮逻辑:
tsx
"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 我会觉得还比较自然。
因为它有明确职责:
  • 负责复制文本
  • 管理复制成功后的短暂状态反馈
调用方也会更干净:
tsx
const { copied, copy } = useCopyText()
这时候 hook 是在提供一个完整的小能力,而不是单纯把几行状态搬家。

一个我现在会谨慎的例子

比如有人很容易写出这种东西:
tsx
usePageState()
然后里面塞:
  • loading
  • modal 开关
  • 当前选中项
  • 搜索词
  • 排序
  • 分页
  • 请求逻辑
  • reset 方法
这种 hook 看起来很省事,但时间一长通常会越来越重。
因为它的职责边界太宽了。
最后页面不是更简单,而是更依赖这个黑盒。
一旦你要改其中一段逻辑,就很容易牵一发动全身。

我现在更喜欢“小而明确”的 hook

如果真的要抽,我通常更偏向这类方向:
  • useDisclosure
  • useDebouncedValue
  • useCopyText
  • useInfiniteScroll
  • useQueryState
  • useIsMounted
这些 hook 有一个共同点:
  • 解决的问题单一
  • 名字能说明职责
  • 调用方读起来直观
  • 不会轻易绑死某一个页面的全部业务
这种抽法后面通常更稳。

最后

我现在不会再把“抽成 hook”自动等同于“代码更高级”。
因为真正重要的不是有没有抽,而是抽完之后,代码有没有更清楚。
如果一段逻辑还在长、还在变、还没稳定, 那暂时放在原地,很多时候比急着抽更健康。
如果它已经在多个地方形成稳定模式,而且抽出来之后能明确表达一个能力, 那 hook 就会非常好用。
如果一句话总结我现在的习惯,就是:
不是为了复用而抽,而是等模式长出来之后,再把它收成一个清楚的能力。

信息

文章信息

自定义 hook 很方便,但不是所有重复代码都值得立刻抽。本文结合表单、列表请求、弹窗控制和局部状态这些常见场景,聊聊我现在怎么判断一段 React 逻辑到底该不该抽成 hook。

最后更新于 2026-05-09阅读时长 8 分钟
React