- Published on
React Hook 专题:useCallback
- Authors

- Name
- 葛颂
useCallback 用来在依赖不变时返回同一个函数引用。它本质上是「缓存函数」的 useMemo 语法糖,同样是性能优化,不是业务语义保证。
useCallback(fn, deps) ≈ useMemo(() => fn, deps)
参考:useCallback、useMemo。
基本用法
import { useCallback, useState, memo } from 'react'
const Item = memo(function Item({ onSelect }: { onSelect: (id: string) => void }) {
// ...
})
function List({ items }: { items: ItemType[] }) {
const [selected, setSelected] = useState<string | null>(null)
const onSelect = useCallback((id: string) => {
setSelected(id)
}, [])
return items.map((item) => <Item key={item.id} onSelect={onSelect} />)
}
没有 useCallback 时,父组件每次渲染都会新建 onSelect,即便 Item 包了 memo,也会因 props 变化而重渲染。
原理
- 渲染时用
Object.is比较依赖。 - 依赖未变 → 返回上一次缓存的函数引用(函数体闭包仍来自创建该函数的那次渲染——见下文「陈旧闭包」)。
- 依赖变了 → 用当前渲染的新函数替换缓存。
因此:useCallback 稳定的是引用;函数内部读到的 props/state,取决于你如何写依赖与更新逻辑。
什么时候值得用
- 把回调传给
memo子组件,且子组件重渲染成本高。 - 回调出现在其他 Hook 的依赖里(如
useEffect(..., [onChange])),避免父级重渲染导致 Effect 无意义重跑。 - 自定义 Hook 对外暴露稳定 API,减少消费方抖动。
不值得用:
- 传给普通 DOM:
<button onClick={() => ...} />(浏览器不在乎引用是否变化)。 - 子组件没有
memo/ 本来就会因其他 props 常刷。 - 依赖数组每次都变——等于白写。
- 全面手写「防御性 useCallback」而不看 profiler(可读性差,收益低;有 React Compiler 时更没必要默认包)。
与 useMemo 怎么选
| 需求 | 用 |
|---|---|
| 缓存计算结果(数组、对象、数字) | useMemo |
| 缓存函数 | useCallback |
| 缓存「工厂创建的对象且含方法」 | 通常 useMemo(() => ({ ... }), deps) |
不要套娃炫技:useMemo(() => () => doSomething(), deps) 直接写成 useCallback。
常见问题与解决办法
1. 陈旧闭包:引用稳定了,值却是旧的
const [count, setCount] = useState(0)
// ❌ deps 为 [],onClick 永远看到 count === 0
const onClick = useCallback(() => {
console.log(count)
setCount(count + 1)
}, [])
办法:
- 把会变的值加入依赖:
[count](引用会随 count 变——这是正确的权衡)。 - 或用函数式更新,不再闭合旧 state:
const onClick = useCallback(() => {
setCount((c) => c + 1)
}, [])
- Effect 里「要最新值、但不想重启订阅」时,优先考虑
useEffectEvent,而不是强行[]+ 读过期闭包。
2. 依赖里塞了每次新建的对象
const options = { debounce: 300 } // 每次新引用
const fetchData = useCallback(() => api.load(options), [options]) // 形同虚设
办法:依赖原始值 debounce,或把 options 提到模块作用域 / useMemo。
3. 以为 useCallback 能减少「自己的」计算量
useCallback 不跳过函数体内的逻辑;它只避免「新建函数」和由此引发的子树更新。函数被调用时,照样执行全部逻辑。
贵的是计算过程 → 用 useMemo 或把重活移出渲染路径;不是给事件处理再套一层空 useCallback。
4. 和 useEffect 互相「打补丁」
父组件用 useCallback 稳定 onMessage,子组件 useEffect(() => subscribe(onMessage), [onMessage])——这是合理模式。
但若 onMessage 依赖一大串 state,引用仍频繁变,Effect 照样狂跑。此时应:
- 收窄依赖;
- 把订阅参数拆成稳定的 id + Effect Event 读最新回调;
- 或重新考虑数据流(谁拥有订阅)。
5. 在列表里给每一项 useCallback
Hook 不能放在循环里。为每行绑定参数时:
// ✅ 行组件自己处理,或
<button onClick={() => onSelect(item.id)} />
// 若必须稳定:让 Item 接收 id + 稳定的 onSelect(id)
6. 过度 memo 导致代码噪音
团队规范若要求「所有回调必须 useCallback」,往往会制造错误依赖与虚假安全感。
办法:以测量为准;约定「跨 memo 边界 / 进 Effect 依赖」才使用;Compiler 项目减少手写。
实践清单
- 先确认消费者是否在乎引用相等(
memo、Effect 依赖、依赖比较的上下文)。 - 写全依赖;用函数式更新减少不必要的 deps。
- 不要用它修复「本不该放进依赖的内联对象」——应重构数据形状。
- 记住:稳定引用 ≠ 最新闭包值。
- 与
useMemo职责分清;能不用就不用。