516 字
3 分钟
useCallback

它缓存的是函数引用#

const handleSubmit = useCallback(orderDetails => {
post('/orders', { productId, orderDetails })
}, [productId])

函数字面量每次渲染都是新对象。这件事本身无害,创建函数很快。只有当这个引用被用来判断「要不要重做某件事」时,稳住它才有意义。

两种情况值得用#

第一种,传给被 memo 包起来的子组件:

const List = memo(function List({ items, onSelect }) {
// ...
})
function App({ items }) {
const [query, setQuery] = useState('')
const handleSelect = useCallback(id => {
// ...
}, [])
return <List items={items} onSelect={handleSelect} />
}

子组件没被 memo 包,或者 props 里还有别的每次都新的值,这个 useCallback 就起不到作用。

第二种,作为 Effect 或其他 Hook 的依赖:

const load = useCallback(async () => {
const res = await fetch(`/api/users/${userId}`)
setUser(await res.json())
}, [userId])
useEffect(() => {
load()
}, [load])

这段更常见的简化写法是把请求直接写进 Effect 里,连这层都省掉。真需要把函数往外传时才用 useCallback。

与 useEffectEvent 的分工#

如果这个函数其实是「某个动作」,只是被 Effect 用到,用 useEffectEvent 更准:它能读到最新的 props 与 state,又不必进依赖数组。

与 useMemo 的关系#

useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。缓存值用 useMemo,缓存函数用 useCallback,后者只是省掉一层嵌套函数,别的没有区别。

React Compiler 之后#

编译器会自动记忆化函数(见 useMemo 参考页开头的注记),新代码不必默认手写。开了编译器的项目里,useCallback 主要留给需要精确控制引用的地方。

四条边界#

依赖要写全。lint 的 exhaustive-deps 规则会检查;漏了会读到旧值,多了会每次重建,等于没缓存。

官方不主张到处用它。给一个没包 memo 的子组件传 useCallback 包过的函数,只是白加一层。

它不改变执行结果,只稳住引用。

依赖里放了每次新建的对象或函数,缓存照样失效。要么把那个值也稳住,要么把它移到组件外,或者干脆塞进 Effect 里。

第 27 篇,共 39 篇本文属于系列

useCallback
https://www.choria.top/posts/hook-use-callback/
作者
乔瑞雅·克林威尔
发布于
2026-05-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 190 天

部分内容可能已过时