自定义 Hook,不是少写几行代码

0 阅读6分钟

如果你做过 React 代码评审,大概率见过这种场面:一个组件里塞了三四个 useEffect,有监听窗口大小的、有轮询接口的、有订阅 WebSocket 的——整个组件像一台裸露的机房,线缆横七竖八,你根本分不清哪根线连着哪台设备。

这时候有人说:"提个自定义 Hook 吧。"

但问题是——你提 Hook 的目的是什么?是"少写几行代码",还是让组件只表达意图?

这两个答案,决定了你写出的 Hook 是真正的架构升级,还是把烂代码换了个文件而已。React 官方文档里有一篇 Reusing Logic with Custom Hooks,把这件事说得非常透彻。我今天帮你把核心逻辑串起来。

一、Hook 是"隔断墙",不是"搬运工"

先用一个建筑学类比帮你建立直觉。

组件是房间的承重墙——负责结构(JSX)和表达意图。而自定义 Hook 是隔断层——水管、电线、网线全部走在隔断里面,住户只看到墙上的开关和插座。

看 React 官方的 useOnlineStatus 例子:

// ❌ 管线裸露在组件里
function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() { setIsOnline(true); }
    function handleOffline() { setIsOnline(false); }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
// ✅ 管线藏进隔断,组件只看到开关
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

提取前,组件同时做了两件事:表达 UI 意图 + 处理浏览器事件订阅。提取后,组件只剩下一件事:声明"我需要知道网络状态"

好 Hook 的价值不是"少写几行",而是让组件从"怎么做"退回到"想要什么"。

二、共享逻辑 ≠ 共享状态

这是新手最容易踩的认知坑。

两个组件都调用了 useOnlineStatus(),网络断开时它们同时显示离线——这是不是说它们"共享了一个 state"?

不是。  它们各自拥有独立的 state 变量和独立的 Effect。恰好显示相同值,是因为它们同步的是同一个外部事实(网络是否在线)。

React 文档用 useFormInput 说得更直白:

function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');
  // 两次调用 = 两个独立的 state,互不干扰
}

用生物学的话说:自定义 Hook 更像细胞膜——它定义了一层"选择性透过接口",决定什么进来(参数)、什么出去(返回值)。但每个细胞有自己的膜,各自独立运作。

如果你真正需要多个组件共享同一份状态,答案是状态提升或 Context——不是 Hook。

三、命名的秘密:能力动词 vs 时间副词

这是整篇文档中我认为最精华的设计原则。

React 官方列了一张对比表:

✅ 好名字🔴 坏名字
useOnlineStatus()useMount(fn)
useChatRoom(options)useEffectOnce(fn)
useData(url)useUpdateEffect(fn)
useIntersectionObserver(ref)useLifecycle(fn)

从语言学角度看,好 Hook 用的是能力动词——描述"这个 Hook 赋予组件什么能力":感知网络状态、连接聊天室、获取数据。

坏 Hook 用的是时间副词——描述"代码在什么时机运行":挂载时、仅一次、更新时。

围绕时机命名的 Hook,本质是把 useEffect 包了一层皮,还让 linter 失去了审查能力。  官方文档举了个例子:

// 🔴 useMount 隐藏了依赖项问题
function ChatRoom({ roomId }) {
  useMount(() => {
    const connection = createConnection({ roomId, serverUrl });
    connection.connect();
  });
}

function useMount(fn) {
  useEffect(() => {
    fn();
  }, []); // 🔴 linter 会警告 fn 缺失,但你已经看不到了
}

roomId 变了,连接不会重建——这个 bug 因为被 useMount 包了一层,变得更难发现。

Hook 的命名应回答"做什么",而不是"何时做"。

四、数据流要显式:进什么、出什么

好 Hook 像一个设计精良的函数签名——看名字和参数就知道数据如何流动。

// 输入:url → 输出:data
const cities = useData(`/api/cities?country=${country}`);

// 输入:roomId + serverUrl → 效果:连接聊天室
useChatRoom({ roomId, serverUrl });

官方文档还展示了一个进阶技巧:Hook 之间可以形成数据管道——前一个 Hook 的输出"馈入"下一个 Hook 的输入:

function ShippingForm({ country }) {
  const cities = useData(`/api/cities?country=${country}`);
  const [city, setCity] = useState(null);
  const areas = useData(city ? `/api/areas?city=${city}` : null);
}

这比原来在组件里写两坨 useEffect(各自管 ignore 标记和竞态清理)清晰得多。组件变成了一条声明式的数据流管道,每一步都可以独立推理。

当你能用"输入→输出"描述一个 Hook 时,说明它的抽象边界是对的。

五、Hook 是升级通道,不是终点

React 文档透露了一个容易被忽略的战略意图:把 Effect 包进 Hook,是为了将来能更容易替换底层实现。

比如 useOnlineStatus 最初用 useState + useEffect 实现,后来 React 提供了专门的 useSyncExternalStore,只需要改 Hook 内部,所有使用它的组件一行都不用动:

// 升级后:更健壮的实现(处理了 SSR、挂载前离线等边界情况)
export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true // 服务端渲染时的值
  );
}

调用方完全无感。这就是抽象的复利效应——今天多花 5 分钟把 Effect 包进 Hook,明天迁移时能省 50 个文件的改动。

同样的逻辑适用于 React 19 的 use API 和 Server Components。当数据获取不再需要 useEffect + fetch,你只需改 useData 内部,整个应用平滑迁移。

六、判断框架:什么时候该提 Hook?

场景该提 Hook 吗理由
两个组件有重复的 Effect(同步同一类外部系统)提取共同逻辑,让组件只表达意图
单个组件里 Effect 太长、太复杂即使没有复用需求,隐藏细节也提升可读性
纯计算逻辑(排序、过滤、格式化)用普通函数,不需要 use 前缀
想包装 useEffect(fn, []) 叫 useMount时机命名 = 伪抽象,隐藏了依赖项问题
未来可能切换底层实现(如从 fetch 迁移到 RSC)Hook 是升级通道,早包早受益
仅为了"看起来整洁"而提取一行 useState没有逻辑复杂度,提取反而增加跳转成本

七、一个思维实验:如果 React 没有 Hook

想象一下:如果你需要"订阅网络状态"这个能力,但 React 不提供 Hook 机制,你会怎么做?

大概率你会写一个高阶组件(HOC)或一个 render props 组件。但那样做有两个问题:组件树嵌套变深、数据流变得不透明。

自定义 Hook 的本质优势是:它不增加组件层级,却能把外部系统的复杂性完全隔离在一个函数调用里。  它比 HOC 更轻量,比 render props 更直观,比全局状态管理更局部。

所以下次你犹豫"要不要提一个 Hook"时,换个问法:

"如果我在读这个组件,我是希望看到浏览器 API 的订阅细节,还是只想看到'这里需要知道网络状态'?"

如果答案是后者——提取它。


如果你只想带走一句话,我建议记这个:

自定义 Hook 不是"少写几行"的快捷方式,而是一层能力接口——它让组件只表达意图,把"怎么做"藏在隔断墙后面。

参考原文:

• React 团队 — Reusing Logic with Custom Hooks

qrcode_for_gh_6a9e7f3719d6_344.jpg