实战中的自定义 Hook
Hook 这章介绍了基本概念:自定义 Hook 是以 use 开头的函数,将多个 Hook 组合成可复用的逻辑。这章讲的是实战中这样做的样子,课程用一个很有说服力的例子说明:在花了整个章节构建 无头 Toggle 组件 之后,一个自定义 Hook 用少得多的代码替代了它,然后这个组件就被删除了。
关键的区分在于普通的 工具函数。一个获取数据或格式化日期的函数是工具函数。而自定义 Hook 会调用其他 Hook,这就是为什么 Hook 的规则 在它内部也适用。
提取 effect:useEffectOnUpdate
Toggle 组件最后有一块逻辑,它的作用是"当状态改变时运行这个回调,但首次渲染时不运行":一个用 firstRender ref 保护的 useEffect。这不是 Toggle 逻辑,而是 effect 逻辑,在任何地方都有用。把它提取出来会得到一个签名和 useEffect 本身一样的 Hook:
export default function useEffectOnUpdate(effectFunction, deps) {
const firstRender = useRef(true)
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
effectFunction()
}
}, deps)
}调用者写 useEffectOnUpdate(onToggle, [on]),首次渲染的记账逻辑就从他们的代码里消失了。这种包装其他 Hook 的 Hook 有时值得编写,即使被包装的逻辑只有几行,因为这个名字表达了意图。
这个守卫有一个仅在开发环境的细节
和 render props 标记的一样:StrictMode 会在它的第一次 effect 运行时耗尽 ref,所以回调会在第二次才触发。生产构建不会这样做,更持久的修复方法是对比值和之前的值,而不是跟踪这是否是首次渲染。
提取状态:useToggle
useEffectOnUpdate 什么都不返回,就像 useEffect。大多数自定义 Hook 会返回东西,就像 useState 一样,而返回的东西的形状是一个设计决策。useToggle 把布尔值、翻转函数和变化回调聚在一起:
export default function useToggle({ initialValue = false, onToggle = () => {} } = {}) {
const [on, setOn] = useState(initialValue)
const toggle = () => setOn(prevOn => !prevOn)
useEffectOnUpdate(onToggle, [on])
return [on, toggle]
}const [on, toggle] = useToggle({ initialValue: true })每次调用 useToggle 都会得到它自己的 useState,所以两个调用者各自拥有两个独立的布尔值,不管是两个组件还是一个组件调用了这个 Hook 两次。
这里有三个决策,每一个都是你会反复用到的模式:
- 返回一个数组 当这些值是一个自然配对、调用者会给它们重新命名的时候,就像
useState一样:一个调用者解构[on, toggle],另一个解构[open, toggleOpen]。当有多个值且名字比顺序更重要时,返回一个对象。 - 接收一个配置对象 而不是位置参数。用
useToggle(false, callback)的话,一个只想要回调的调用者也被迫先提供布尔值;用{ initialValue, onToggle }的话他们传自己需要的东西,跳过其余的,每个选项都可以有默认值。 - 回调默认为 noop(
() => {}),这样 Hook 可以无条件地调用onToggle(),不会让那些从没传过回调的调用者崩溃。
收益:删除组件
有了 useToggle,星星小部件完全不需要任何 Toggle 机制了:
export default function Star({ onChange }) {
const [on, toggle] = useToggle({ onToggle: onChange })
return on
? <BsStarFill className="star filled" onClick={toggle} />
: <BsStar className="star" onClick={toggle} />
}菜单保持它的 复合组件 结构,因为它的各部分还是需要共享的隐式状态,但它的提供者现在从 Hook 获取值:Menu 调用 useToggle,通过它自己的 context 传递 { open, toggleOpen },然后无头 Toggle 组件的文件就被删除了。
这个删除就是这个章节的真正课程。无头组件在很多复合组件可能会共享它时是值得的;一旦逻辑能装进一个 Hook,Hook 就是更简单的工具,删除你为之骄傲的可工作代码是这份工作的常规部分。
这两种模式是互补的:Hook 共享逻辑,无头组件通过 context 和组合在树中共享逻辑。优先考虑 Hook,让组件赚取额外的仪式感。课程用一个独立项目 Component Library++ 来结束这个章节,它把这五章的每个模式都放在一个库里了。
use 开头的函数,它基于 useState 这样的 Hook 来打包一段逻辑。一旦 useToggle 存在了,任何组件都可以在一行里获得一个开关值和一个翻转函数,就像 useState 给你一个值和一个 setter 一样。 每个调用它的组件都会得到它自己的状态副本,所以 Hook 是你复用的配方而不是大家共享的值。
接下来:路由,URL 开始决定哪些组件渲染。

