Menu

React 自定义 Hook:编写方法与示例

自定义 Hook 是一个名字以 use 开头、并调用其他 Hook 的函数,让多个组件能复用同一段有状态的逻辑。动手编写 useToggle、useLocalStorage、useDebounce 和 useWindowWidth,并理解为什么每次调用都有自己的状态。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

自定义 Hook 是一个名字以 use 开头、并调用其他 Hook 的函数。当两个组件需要同样的有状态逻辑时,就可以写一个:把 useState 和 useEffect 调用移进这个函数,返回组件需要的东西,再在每个组件里调用它。

两个剧透框都调用了 useToggle,打开其中一个,另一个仍然是关闭的。每次调用都会执行自己的 useState,所以每个组件都有自己的值。自定义 Hook 共享的是逻辑,不是状态。

编写规则

  • 名字以 use 加一个大写字母开头:useToggle、useOnlineStatus。Hook 的 lint 规则靠这个前缀找到 Hook 调用并进行检查。
  • 至少调用一个 Hook。 不调用任何 Hook 的辅助函数只是普通函数;把它命名为 formatDate 而不是 useFormatDate,这样它在任何地方都能调用,包括条件语句里。
  • 遵守 Hook 的规则:在它的顶层调用 Hook,并且只在组件或其他自定义 Hook 中调用它。
  • 返回有用的东西。 数组([on, toggle])方便调用方给值重命名;三个及以上的值用对象({ data, loading, error })读起来更清楚。

useLocalStorage:刷新后依然保留的状态

useState 在刷新后会忘掉一切。这个 Hook 从 localStorage 读取初始值,并把每次改动写回去。

预览运行在一个沙盒框架中,浏览器在这里会阻止 localStorage,所以你会在 Console 里看到警告,Hook 则退回到普通的 state。在正常页面中,名字在刷新后依然保留。try/catch 不只是为了这个预览:在真实应用中,无痕浏览模式、被禁用的 Cookie 和存储配额已满都会让存储抛出错误。给 useState 传入一个函数,意味着存储只在首次渲染时读取一次。

useDebounce:等用户停止输入

搜索框不应该每按一次键就发一次请求。useDebounce 返回一个值的副本,只有在这个值停止变化并经过设定的延迟后才更新。

快速输入一个词:Typed 随每次按键变化,Debounced 在你停下半秒后才跟上,Console 只记录一次搜索。每次按键都会改变 value,所以 React 会先执行清理函数(clearTimeout),再启动新的定时器。把 500 改成 0,每次按键都会记录一次搜索。

useWindowWidth:订阅并清理

监听浏览器事件的 Hook 必须在组件消失时移除监听器,否则每次挂载都会留下一个。

调整浏览器窗口大小,数字会随之变化。点击按钮,Console 会显示监听器被移除。把副作用里的 return 删掉,然后挂载、卸载几次:每次都会出现一行 added,却没有 removed,旧的监听器越积越多。对于这类浏览器的值,useSyncExternalStore 是 React 专门为此打造的 Hook,很适合在自定义 Hook 里调用。

共享的是逻辑,不是状态

两个组件调用同一个 Hook,会得到 Hook 内部所有东西的两份独立副本。这会让那些以为 Hook 像全局变量一样工作的人感到意外。

点击任意一个按钮,只有对应的计数器会变化。如果两边应该显示同一个数字,就把状态移到 App 里再向下传递,或者放进 context。自定义 Hook 可以为了方便而封装这个共享状态(useCart() 读取一个 CartContext),但共享来自 context,而不是 Hook。

什么时候写自定义 Hook

当同一组 Hook 调用出现在两个组件里,或者某个组件的副作用逻辑长到用一个名字来说明会更清楚时(useOnlineStatus() 比二十行监听器代码好读得多),就把它提取成 Hook。不要只为了改个名字就把单个 useState 调用包进 Hook,也不要写那种只调用 useEffect 从 props 设置状态的 Hook;应该在渲染时直接计算这个值。

常见问题

React 中的自定义 Hook 是什么?

一个名字以 use 开头、并调用一个或多个其他 Hook 的 JavaScript 函数。它把有状态的逻辑(state、副作用、ref)打包起来,任何组件只要调用这个函数就能复用它。

自定义 Hook 会在组件之间共享状态吗?

不会。每次调用自定义 Hook 都会执行它自己的 useState 和 useEffect,所以每个组件得到的是独立的状态。要在组件之间共享同一个值,就把状态提升,或者放进 context 或外部 store。

为什么自定义 Hook 的名字必须以 use 开头?

这个名字告诉 React 的 lint 规则和其他开发者:这个函数会调用 Hook,所以 Hook 的规则也适用于它:只在顶层调用,绝不放在条件或循环里。没有这个前缀,linter 就无法检查它内部的调用。

自定义 Hook 可以返回 JSX 吗?

它可以返回任何东西,但如果返回的是 JSX,通常写成组件会更好。自定义 Hook 一般返回值和函数,形式是数组([value, toggle])或对象({ data, error, loading })。

可以在 useEffect 或事件处理函数里调用自定义 Hook 吗?

不可以。自定义 Hook 仍然是 Hook,必须遵守 Hook 的规则:在组件或另一个自定义 Hook 的顶层调用。

Coddy 编程语言插图

和 Coddy 一起学编程

开始