在 React 开发中,useCallback 是最容易被误用(Overused)的 Hook 之一。很多开发者看到组件重渲染(Re-render),下意识地就想把所有函数都包上一层 useCallback,认为这样能提升性能。
但事实往往相反:在错误的地方使用 useCallback,不仅不能优化性能,反而会增加内存开销和代码复杂度。
今天我们结合 Hacker News 搜索代码,来拆解 useCallback 到底解决了什么问题,以及什么时候才应该用它。
1. 案发现场:代码真的需要优化吗?
让我们先看代码中的这一部分:
JavaScript
// App.js (原始代码)
export default function App() {
const [searchTerm, setSearchTerm] = React.useState("js");
// ❌ 疑问:这里是否需要 useCallback?
const handleChange = (e) => {
setSearchTerm(e.target.value);
};
return (
<form>
{/* 这里的 input 是原生 DOM 标签 */}
<input onChange={handleChange} ... />
</form>
);
}
现状分析:
- 当用户输入字符,
handleChange执行 ->setSearchTerm更新状态。 App组件触发重渲染(Re-render)。- 在这次新的渲染中,
handleChange函数被重新创建(在内存中生成了一个全新的函数引用)。 - 这个新函数被传递给
<input>标签。
结论:
在你的当前代码中,完全不需要 useCallback。
原因:
接收 handleChange 的是 <input>,这是一个原生 DOM 元素。原生元素不具备"通过对比 Props 来决定是否更新"的能力。无论你传给它的是旧函数还是新函数,只要父组件渲染,React 都会重新把事件绑定更新一遍。
在这里加 useCallback,就像是给一次性纸杯买保险------成本(缓存机制、依赖对比的计算量)支出了,但没有任何收益。
2. 核心概念:引用相等性 (Referential Equality)
要理解 useCallback,必须理解 JavaScript 中的一个基础概念:
JavaScript
const functionA = () => { console.log('hi'); };
const functionB = () => { console.log('hi'); };
console.log(functionA === functionB); // false ❌
console.log(functionA === functionA); // true ✅
在 React 函数组件中,每次渲染,组件内部定义的函数都会被重新创建。虽然代码逻辑没变,但在计算机内存里,它已经是一个全新的对象了。
useCallback 的唯一作用就是:在多次渲染之间,强行保留同一个函数引用,只要依赖项不变,它返回的永远是内存里的同一个地址。
3. 什么时候才需要它?(引入 React.memo)
只有当这个函数被传递给经过优化的子组件 时,useCallback 才是必须的。
假设随着项目变大,你把 <input> 封装成了一个独立的、功能复杂的组件 FancyInput,并且为了性能,你使用了 React.memo。
场景 A:有 memo,但没用 useCallback (无效优化)
JavaScript
// 这是一个被 memo 保护的组件
// 它的原则是:只有 props 变了,我才重新渲染
const FancyInput = React.memo(function FancyInput({ onChange, value }) {
console.log("FancyInput 渲染了!");
return <input className="fancy" onChange={onChange} value={value} />;
});
export default function App() {
const [searchTerm, setSearchTerm] = React.useState("js");
// 每次 App 渲染,这里都会生成一个新的函数地址
const handleChange = (e) => setSearchTerm(e.target.value);
return (
<>
{/* 悲剧发生在这里:
尽管 searchTerm 没变 (假设是其他 state 触发了 App 更新),
但因为 handleChange 的内存地址变了,
React.memo 认为 props.onChange 变了。
结果:FancyInput 依然会强制重渲染!
*/}
<FancyInput onChange={handleChange} value={searchTerm} />
</>
);
}
场景 B:memo + useCallback (黄金搭档)
这时候,useCallback 就要登场了。它是为了配合 React.memo 工作的。
JavaScript
export default function App() {
const [searchTerm, setSearchTerm] = React.useState("js");
// ✅ 正确使用:缓存函数引用
const handleChange = React.useCallback((e) => {
setSearchTerm(e.target.value);
}, []); // 依赖项为空,永远不重建
return (
{/* 现在,当 App 因为其他原因重渲染时,
handleChange 还是原来的内存地址。
React.memo 发现 props 没变,于是跳过 FancyInput 的渲染。
性能提升达成!
*/}
<FancyInput onChange={handleChange} value={searchTerm} />
);
}
4. 另一个场景:作为 useEffect 的依赖
代码中其实有一个潜在的地方可能需要 useCallback,那就是当函数本身被放在 useEffect 的依赖数组里时。
JavaScript
// 假设这是定义在组件内的函数
const fetchNews = async (query) => {
const data = await searchHackerNews(query);
setResults(data.hits);
};
useEffect(() => {
fetchNews(debouncedSearchTerm);
}, [debouncedSearchTerm, fetchNews]); // ⚠️ fetchNews 是依赖项
如果fetchNews 不包裹 useCallback,每次渲染 fetchNews 都会变成新函数,导致 useEffect 认为依赖变了,从而无限循环 或者不必要的频繁执行。
在这种情况下,必须使用 useCallback 锁住 fetchNews。
总结:决策清单
回到代码,请按照这个清单来决定是否使用 useCallback:
-
这个函数是传给原生 DOM (div, button, input) 的吗?
- 是 -> 不用 (用了也没用)。
- 否 -> 看下一条。
-
这个函数是传给子组件的,且子组件用了
React.memo吗?- 是 -> 用 (为了让 memo 生效)。
- 否 -> 不用 (大部分子组件都很轻量,不需要 memo)。
-
这个函数会被作为
useEffect或其他 Hook 的依赖项吗?- 是 -> 用 (防止死循环或频繁触发 Effect)。
最终建议:
在App 组件当前的状态下,保持原样是最好的选择。代码清晰、逻辑简单,没有任何不必要的性能开销。