useCallback

useCallback 是一个 React Hook,它可以让你在重新渲染之间缓存一个函数定义。

const cachedFn = useCallback(fn, dependencies)

注意

React 编译器 会自动缓存值和函数,从而减少手动 useCallback 调用的需求。你可以使用编译器自动处理缓存。


参考

🌐 Reference

useCallback(fn, dependencies)

在组件的顶层调用 useCallback 来在重新渲染之间缓存函数定义:

🌐 Call useCallback at the top level of your component to cache a function definition between re-renders:

import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);

查看更多示例。

参数

🌐 Parameters

  • fn:你想要缓存的函数值。它可以接受任意参数并返回任意值。React 会在初次渲染期间将你的函数返回给你(不会调用!)。在后续渲染中,如果 dependencies 自上次渲染以来没有改变,React 会再次给你相同的函数。否则,它会给你当前渲染中传入的函数,并将其存储以备后续重用。React 不会调用你的函数。函数会返回给你,以便你决定何时以及是否调用它。
  • dependenciesfn 代码中引用的所有响应式值的列表。响应式值包括 props、state,以及在组件主体中直接声明的所有变量和函数。如果你的代码检查工具已针对 React 配置,它会验证每个响应式值是否被正确指定为依赖。依赖列表必须具有固定数量的项目,并像 [dep1, dep2, dep3] 一样内联编写。React 将使用 Object.is 比较算法,将每个依赖与其先前的值进行比较。

返回

🌐 Returns

在初始渲染时,useCallback 会返回你传入的 fn 函数。

🌐 On the initial render, useCallback returns the fn function you have passed.

在随后的渲染中,它要么返回上次渲染中已经存储的 fn 函数(如果依赖没有变化),要么返回你在本次渲染中传入的 fn 函数。

🌐 During subsequent renders, it will either return an already stored fn function from the last render (if the dependencies haven’t changed), or return the fn function you have passed during this render.

注意事项

🌐 Caveats

  • useCallback 是一个 Hook,因此你只能在你的组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,可以提取一个新组件并将状态移入其中。
  • React 不会丢弃缓存的函数,除非有特定的原因这么做。 例如,在开发环境中,当你编辑组件文件时,React 会丢弃缓存。在开发环境和生产环境中,如果你的组件在初次挂载时挂起,React 也会丢弃缓存。未来,React 可能会增加更多利用丢弃缓存的功能——例如,如果未来 React 对虚拟列表提供内置支持,那么丢弃滚出虚拟表视口的项目的缓存是有意义的。如果你依赖 useCallback 作为性能优化,这应该符合你的预期。否则,状态变量ref 可能更合适。

用法

🌐 Usage

跳过组件的重新渲染

🌐 Skipping re-rendering of components

当你优化渲染性能时,有时你需要缓存传递给子组件的函数。让我们先看看如何实现的语法,然后再看看在什么情况下它是有用的。

🌐 When you optimize rendering performance, you will sometimes need to cache the functions that you pass to child components. Let’s first look at the syntax for how to do this, and then see in which cases it’s useful.

要在组件重新渲染之间缓存函数,请将其定义封装到 useCallback Hook 中:

🌐 To cache a function between re-renders of your component, wrap its definition into the useCallback Hook:

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...

你需要向 useCallback 传递两样东西:

🌐 You need to pass two things to useCallback:

  1. 要在重新渲染之间缓存的函数定义。
  2. 一个依赖列表,包括你组件中函数使用的每一个值。

在初始渲染时,你从 useCallback 返回的函数将是你传入的函数。

在接下来的渲染中,React 将会把 依赖 与你在上一次渲染中传入的依赖进行比较。如果没有依赖发生变化(与Object.is相比),useCallback 将返回与之前相同的函数。否则,useCallback 将返回你在本次渲染中传入的函数。

换句话说,useCallback 会在重新渲染之间缓存一个函数,直到它的依赖发生变化。

🌐 In other words, useCallback caches a function between re-renders until its dependencies change.

让我们通过一个例子来看看这什么时候有用。

假设你正在将一个 handleSubmit 函数从 ProductPage 组件传递到 ShippingForm 组件:

🌐 Say you’re passing a handleSubmit function down from the ProductPage to the ShippingForm component:

function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);

你已经注意到,切换 theme 属性会让应用暂时冻结,但如果你从 JSX 中移除 <ShippingForm />,它会感觉很快。这告诉你值得尝试优化 ShippingForm 组件。

🌐 You’ve noticed that toggling the theme prop freezes the app for a moment, but if you remove <ShippingForm /> from your JSX, it feels fast. This tells you that it’s worth trying to optimize the ShippingForm component.

默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件。 这就是为什么当 ProductPage 使用不同的 theme 重新渲染时,ShippingForm 组件会重新渲染。这对于不需要大量计算的组件来说是可以的。但是,如果你发现重新渲染很慢,你可以通过将 ShippingForm 封装在 memo: 中来告诉它在 props 与上次渲染相同时跳过重新渲染。

import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});

有了这个改变,如果 ShippingForm 的所有属性与上一次渲染时完全相同,它将跳过重新渲染。 这时候缓存一个函数就很重要了!假设你定义了 handleSubmit,但没有 useCallback:

function ProductPage({ productId, referrer, theme }) {
// Every time the theme changes, this will be a different function...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}

return (
<div className={theme}>

<ShippingForm onSubmit={handleSubmit} />
</div>
);
}

在 JavaScript 中,function () {}() => {} 总是创建一个不同的函数, 类似于 {} 对象字面量总是创建一个新对象。通常,这不会成为问题,但这意味着 ShippingForm 属性永远不会相同,并且你的 memo 优化将不起作用。这就是 useCallback 派上用场的地方:

function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...

return (
<div className={theme}>

<ShippingForm onSubmit={handleSubmit} />
</div>
);
}

通过将 handleSubmit 封装在 useCallback 中,你可以确保它在重新渲染之间保持相同的函数(直到依赖发生变化)。你不必需将函数封装在 useCallback 中,除非你有某些特定的原因。在这个示例中,原因是你将它传递给一个被 memo 封装的组件,这样可以让它跳过重新渲染。还有其他你可能需要 useCallback 的原因,这些原因在本页后面会有描述。

注意

你只应该将 useCallback 作为性能优化手段来依赖。 如果你的代码在没有它的情况下无法运行,先找到潜在问题并修复它。然后你可以再添加 useCallback

深入研究

🌐 How is useCallback related to useMemo?

你会经常看到 useMemouseCallback 一起使用。当你尝试优化子组件时,它们都很有用。它们让你能够 memoize(换句话说,就是缓存)你传递下去的内容:

🌐 You will often see useMemo alongside useCallback. They are both useful when you’re trying to optimize a child component. They let you memoize (or, in other words, cache) something you’re passing down:

import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);

const requirements = useMemo(() => { // Calls your function and caches its result
return computeRequirements(product);
}, [product]);

const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);

return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}

区别在于他们让你缓存的内容:

🌐 The difference is in what they’re letting you cache:

  • useMemo 会缓存调用你的函数的 结果 在这个例子中,它缓存了调用 computeRequirements(product) 的结果,因此除非 product 改变,否则结果不会变化。这使你可以传递 requirements 对象而不会导致 ShippingForm 的不必要重新渲染。在必要时,React 会在渲染过程中调用你传入的函数来计算结果。
  • useCallback 缓存 函数本身。useMemo 不同,它不会调用你提供的函数。相反,它缓存你提供的函数,因此 handleSubmit 本身 不会改变,除非 productIdreferrer 已改变。这让你可以传递 handleSubmit 函数而不会导致 ShippingForm 不必要的重新渲染。你的代码在用户提交表单前不会运行。

如果你已经熟悉 useMemo,你可能会觉得将 useCallback 理解为以下内容会很有帮助:

🌐 If you’re already familiar with useMemo, you might find it helpful to think of useCallback as this:

// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}

阅读更多关于 useMemouseCallback 的区别。

深入研究

你应该在所有地方添加 useCallback 吗?

🌐 Should you add useCallback everywhere?

如果你的应用像这个网站一样,并且大多数交互是粗略的(比如替换一个页面或整个部分),通常不需要记忆化。另一方面,如果你的应用更像一个绘图编辑器,并且大多数交互是细粒度的(比如移动形状),那么你可能会发现记忆化非常有用。

🌐 If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.

使用 useCallback 缓存函数仅在少数情况下有价值:

🌐 Caching a function with useCallback is only valuable in a few cases:

  • 你将它作为 prop 传递给一个被 memo. 封装的组件。如果值没有变化,你希望跳过重新渲染。Memoization(记忆化)使你的组件只有在依赖发生变化时才重新渲染。
  • 你传递的函数随后被用作某些 Hook 的依赖。例如,另一个被 useCallback 封装的函数依赖它,或者你从 useEffect. 依赖这个函数

在其他情况下,将函数封装在 useCallback 中没有好处。这样做也没有显著的坏处,因此一些团队选择不去考虑单独的情况,而是尽可能地进行记忆化。缺点是代码变得不那么易读。此外,并非所有记忆化都是有效的:一个“总是新的”单一值就足以破坏整个组件的记忆化。

🌐 There is no benefit to wrapping a function in useCallback in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.

请注意,useCallback 并不会阻止创建函数。你总是在创建一个函数(这没问题!),但是如果没有变化,React 会忽略它并返回一个缓存的函数。

🌐 Note that useCallback does not prevent creating the function. You’re always creating a function (and that’s fine!), but React ignores it and gives you back a cached function if nothing changed.

在实践中,通过遵循几个原则,你可以使大量的 memo 化变得不必要:

  1. 当一个组件在视觉上封装其他组件时,让它接受 JSX 作为子组件。 然后,如果封装组件更新了自己的状态,React 就知道其子组件不需要重新渲染。
  2. 优先使用本地状态,不要把状态提升得比必要的程度更高。不要将像表单或某个项目是否被悬停这样的临时状态保存在组件树的顶层或全局状态库中。
  3. 保持你的渲染逻辑纯粹。 如果重新渲染一个组件导致问题或产生一些明显的视觉瑕疵,那就是你的组件中的一个错误!修复这个错误,而不是添加记忆化。
  4. 避免不必要的会更新状态的效果。 大多数 React 应用的性能问题都是由效果引起的一系列更新造成的,这些更新会导致组件不断重新渲染。
  5. 尝试从你的 Effect 中移除不必要的依赖。 例如,与其使用记忆化,通常更简单的方法是将某些对象或函数移动到 Effect 内部或组件外部。

如果某个特定交互仍然感觉滞后,使用 React 开发者工具的分析器 查看哪些组件最能从记忆化中受益,并在需要的地方添加记忆化。这些原则使你的组件更易于调试和理解,所以无论如何都值得遵循它们。从长远来看,我们正在研究 自动进行记忆化 以彻底解决这个问题。

🌐 If a specific interaction still feels laggy, use the React Developer Tools profiler to see which components benefit the most from memoization, and add memoization where needed. These principles make your components easier to debug and understand, so it’s good to follow them in any case. In long term, we’re researching doing memoization automatically to solve this once and for all.

The difference between useCallback and declaring a function directly

例子 1 of 2:
跳过使用 useCallbackmemo 的重新渲染

🌐 Skipping re-rendering with useCallback and memo

在这个例子中,ShippingForm 组件被人为地减慢,以便你可以看到当你渲染的 React 组件真正变慢时会发生什么。尝试增加计数器并切换主题。

🌐 In this example, the ShippingForm component is artificially slowed down so that you can see what happens when a React component you’re rendering is genuinely slow. Try incrementing the counter and toggling the theme.

增加计数器看起来很慢,因为它会强制已经变慢的 ShippingForm 重新渲染。这是预期的,因为计数器已经变化,所以你需要在屏幕上反映用户的新选择。

🌐 Incrementing the counter feels slow because it forces the slowed down ShippingForm to re-render. That’s expected because the counter has changed, and so you need to reflect the user’s new choice on the screen.

接下来,尝试切换主题。多亏了 useCallbackmemo,尽管有人工延迟,它仍然很快! ShippingForm 跳过了重新渲染,因为 handleSubmit 函数没有改变。handleSubmit 函数没有改变,因为自上次渲染以来,productIdreferrer(你的 useCallback 依赖)都没有改变。

🌐 Next, try toggling the theme. Thanks to useCallback together with memo, it’s fast despite the artificial slowdown! ShippingForm skipped re-rendering because the handleSubmit function has not changed. The handleSubmit function has not changed because both productId and referrer (your useCallback dependencies) haven’t changed since last render.

import { useCallback } from 'react';
import ShippingForm from './ShippingForm.js';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

function post(url, data) {
  // Imagine this sends a request...
  console.log('POST /' + url);
  console.log(data);
}


从记忆化回调更新状态

🌐 Updating state from a memoized callback

有时,你可能需要根据记忆化回调中的先前状态更新状态。

🌐 Sometimes, you might need to update state based on previous state from a memoized callback.

这个 handleAddTodo 函数将 todos 指定为依赖,因为它是从中计算下一个待办事项的:

🌐 This handleAddTodo function specifies todos as a dependency because it computes the next todos from it:

function TodoList() {
const [todos, setTodos] = useState([]);

const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...

通常你希望被记忆化的函数依赖尽可能少的内容。当你只读取一些状态来计算下一个状态时,你可以通过传递一个更新函数来去除那个依赖:

🌐 You’ll usually want memoized functions to have as few dependencies as possible. When you read some state only to calculate the next state, you can remove that dependency by passing an updater function instead:

function TodoList() {
const [todos, setTodos] = useState([]);

const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ...

在这里,你没有将 todos 作为依赖并在内部读取它,而是向 React 传递了有关 如何 更新状态(todos => [...todos, newTodo])的指令。阅读有关更新函数的更多信息。

🌐 Here, instead of making todos a dependency and reading it inside, you pass an instruction about how to update the state (todos => [...todos, newTodo]) to React. Read more about updater functions.


防止副作用过于频繁地触发

🌐 Preventing an Effect from firing too often

有时,你可能想从 Effect: 内部调用一个函数

🌐 Sometimes, you might want to call a function from inside an Effect:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...

这会导致一个问题。每个响应式值都必须被声明为你的 Effect 的依赖。 但是,如果你将 createOptions 声明为依赖,它将导致你的 Effect 不断重新连接到聊天室:

🌐 This creates a problem. Every reactive value must be declared as a dependency of your Effect. However, if you declare createOptions as a dependency, it will cause your Effect to constantly reconnect to the chat room:

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// ...

为了解决这个问题,你可以将需要从 Effect 调用的函数封装到 useCallback 中:

🌐 To solve this, you can wrap the function you need to call from an Effect into useCallback:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Only changes when createOptions changes
// ...

这确保了如果 roomId 相同,createOptions 函数在重新渲染之间保持一致。然而,更好的是消除对函数依赖的需求。 将你的函数放在 Effect 内部:

🌐 This ensures that the createOptions function is the same between re-renders if the roomId is the same. However, it’s even better to remove the need for a function dependency. Move your function inside the Effect:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
function createOptions() { // ✅ No need for useCallback or function dependencies!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}

const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...

现在你的代码更简单,不需要 useCallback了解更多关于移除 Effect 依赖的信息。

🌐 Now your code is simpler and doesn’t need useCallback. Learn more about removing Effect dependencies.


优化自定义钩子

🌐 Optimizing a custom Hook

如果你在编写一个 自定义 Hook, 建议将它返回的任何函数封装到 useCallback 中:

🌐 If you’re writing a custom Hook, it’s recommended to wrap any functions that it returns into useCallback:

function useRouter() {
const { dispatch } = useContext(RouterStateContext);

const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);

const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);

return {
navigate,
goBack,
};
}

这确保了钩子的使用者可以在需要时优化他们自己的代码。

🌐 This ensures that the consumers of your Hook can optimize their own code when needed.


故障排除

🌐 Troubleshooting

每次我的组件渲染时,useCallback 都会返回一个不同的函数

🌐 Every time my component renders, useCallback returns a different function

确保你已将依赖数组指定为第二个参数!

🌐 Make sure you’ve specified the dependency array as a second argument!

如果你忘记了依赖数组,useCallback 每次都会返回一个新函数:

🌐 If you forget the dependency array, useCallback will return a new function every time:

function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Returns a new function every time: no dependency array
// ...

这是将依赖数组作为第二个参数传递的更正版本:

🌐 This is the corrected version passing the dependency array as a second argument:

function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Does not return a new function unnecessarily
// ...

如果这没有帮助,那么问题在于至少有一个依赖与上一次渲染不同。你可以通过手动将依赖记录到控制台来调试这个问题:

🌐 If this doesn’t help, then the problem is that at least one of your dependencies is different from the previous render. You can debug this problem by manually logging your dependencies to the console:

const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);

console.log([productId, referrer]);

然后,你可以在控制台中对来自不同重新渲染的数组右键点击,并为它们都选择“存储为全局变量”。假设第一个保存为 temp1,第二个保存为 temp2,你就可以使用浏览器控制台检查两个数组中的每个依赖是否相同:

🌐 You can then right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. Assuming the first one got saved as temp1 and the second one got saved as temp2, you can then use the browser console to check whether each dependency in both arrays is the same:

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

当你发现哪个依赖正在破坏记忆化时,要么找到方法去移除它,要么也将其记忆化

🌐 When you find which dependency is breaking memoization, either find a way to remove it, or memoize it as well.


我需要在循环中为每个列表项调用 useCallback,但这是不允许的

🌐 I need to call useCallback for each list item in a loop, but it’s not allowed

假设 Chart 组件被 memo 封装。当 ReportList 组件重新渲染时,你想跳过列表中每个 Chart 的重新渲染。然而,你不能在循环中调用 useCallback:

🌐 Suppose the Chart component is wrapped in memo. You want to skip re-rendering every Chart in the list when the ReportList component re-renders. However, you can’t call useCallback in a loop:

function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 You can't call useCallback in a loop like this:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);

return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}

相反,为单个项目提取一个组件,并将 useCallback 放在那里:

🌐 Instead, extract a component for an individual item, and put useCallback there:

function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}

function Report({ item }) {
// ✅ Call useCallback at the top level:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);

return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}

或者,你可以在最后一个代码片段中移除 useCallback,而是将 Report 本身封装在 memo. 中。如果 item 属性没有变化,Report 将跳过重新渲染,因此 Chart 也将跳过重新渲染:

🌐 Alternatively, you could remove useCallback in the last snippet and instead wrap Report itself in memo. If the item prop does not change, Report will skip re-rendering, so Chart will skip re-rendering too:

function ReportList({ items }) {
// ...
}

const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}

return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});