useCallback

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

¥useCallback is a React Hook that lets you cache a function definition between re-renders.

const cachedFn = useCallback(fn, dependencies)

参考

¥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]);

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • fn:要缓存的函数值。它可以接受任何参数并返回任何值。React 将在初始渲染期间返回(而不是调用!)你的函数。在下一次渲染中,如果 dependencies 自上次渲染以来没有改变,React 将再次为你提供相同的函数。否则,它将为你提供你在当前渲染期间传递的函数,并将其存储起来以备日后重用。React 不会调用你的函数。该函数返回给你,因此你可以决定何时以及是否调用它。

    ¥fn: The function value that you want to cache. It can take any arguments and return any values. React will return (not call!) your function back to you during the initial render. On next renders, React will give you the same function again if the dependencies have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later. React will not call your function. The function is returned to you so you can decide when and whether to call it.

  • dependenciesfn 代码中引用的所有反应值的列表。反应值包括属性、状态以及直接在组件主体内声明的所有变量和函数。如果你的 linter 是 为 React 配置,它将验证每个反应值是否正确指定为依赖。依赖列表必须具有恒定数量的条目,并且像 [dep1, dep2, dep3] 一样写成内联。React 将使用 Object.is 比较算法将每个依赖与其先前的值进行比较。

    ¥dependencies: The list of all reactive values referenced inside of the fn code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison algorithm.

返回

¥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,所以你只能在你的组件的顶层或者你自己的钩子中调用它。你不能在循环或条件内调用它。如果需要,提取一个新组件并将状态移入其中。

    ¥useCallback is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.

  • React 不会丢弃缓存的函数,除非有特定原因这样做。例如,在开发中,当你编辑组件的文件时,React 会丢弃缓存。在开发和生产中,如果你的组件在初始挂载期间挂起,React 将丢弃缓存。将来,React 可能会添加更多利用丢弃缓存的功能 - 例如,如果 React 将来添加对虚拟化列表的内置支持,则丢弃滚动出虚拟化列表视口的条目的缓存是有意义的。如果你依赖 useCallback 作为性能优化,这应该符合你的期望。否则,状态变量引用 可能更合适。

    ¥React will not throw away the cached function unless there is a specific reason to do that. For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache—for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should match your expectations if you rely on useCallback as a performance optimization. Otherwise, a state variable or a ref may be more appropriate.


用法

¥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 钩子中:

¥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. 要在重新渲染之间缓存的函数定义。

    ¥A function definition that you want to cache between re-renders.

  2. 依赖列表,包括组件中函数中使用的每个值。

    ¥A list of dependencies including every value within your component that’s used inside your function.

在初始渲染中,你将从 useCallback 获得的返回的函数 将是你传递的函数。

¥On the initial render, the returned function you’ll get from useCallback will be the function you passed.

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

¥On the following renders, React will compare the dependencies with the dependencies you passed during the previous render. If none of the dependencies have changed (compared with Object.is), useCallback will return the same function as before. Otherwise, useCallback will return the function you passed on this render.

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

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

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

¥Let’s walk through an example to see when this is useful.

假设你要将 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 中来跳过重新渲染

¥By default, when a component re-renders, React re-renders all of its children recursively. This is why, when ProductPage re-renders with a different theme, the ShippingForm component also re-renders. This is fine for components that don’t require much calculation to re-render. But if you verified a re-render is slow, you can tell ShippingForm to skip re-rendering when its props are the same as on last render by wrapping it in memo:

import { memo } from 'react';

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

通过此更改,如果 ShippingForm 的所有属性都与上次渲染相同,则将跳过重新渲染。这是缓存函数变得重要的时候!假设你定义了没有 useCallbackhandleSubmit

¥With this change, ShippingForm will skip re-rendering if all of its props are the same as on the last render. This is when caching a function becomes important! Let’s say you defined handleSubmit without 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}>
{/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}

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

¥In JavaScript, a function () {} or () => {} always creates a different function, similar to how the {} object literal always creates a new object. Normally, this wouldn’t be a problem, but it means that ShippingForm props will never be the same, and your memo optimization won’t work. This is where useCallback comes in handy:

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 will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}

通过将 handleSubmit 封装在 useCallback 中,你可以确保它在重新渲染之间具有相同的功能(直到依赖发生变化)。除非出于某些特定原因,否则不必将函数封装在 useCallback 中。在此示例中,原因是你将它传递给封装在 memo, 中的组件,这让它可以跳过重新渲染。你可能需要 useCallback 的其他原因将在本页进一步说明。

¥By wrapping handleSubmit in useCallback, you ensure that it’s the same function between the re-renders (until dependencies change). You don’t have to wrap a function in useCallback unless you do it for some specific reason. In this example, the reason is that you pass it to a component wrapped in memo, and this lets it skip re-rendering. There are other reasons you might need useCallback which are described further on this page.

注意

你应该只依赖 useCallback 作为性能优化。如果你的代码没有它就不能工作,找到潜在的问题并首先修复它。然后你可以将 useCallback 添加回来。

¥You should only rely on useCallback as a performance optimization. If your code doesn’t work without it, find the underlying problem and fix it first. Then you may add useCallback back.

深入研究

¥How is useCallback related to useMemo?

你经常会在 useCallback 旁边看到 useMemo。当你尝试优化子组件时,它们都很有用。它们让你 记忆化(或者换句话说,缓存)你传递的东西:

¥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 会调用你在渲染过程中传递的函数来计算结果。

    ¥**useMemo caches the result of calling your function.** In this example, it caches the result of calling computeRequirements(product) so that it doesn’t change unless product has changed. This lets you pass the requirements object down without unnecessarily re-rendering ShippingForm. When necessary, React will call the function you’ve passed during rendering to calculate the result.

  • useCallback 缓存函数本身。不像 useMemo,它不会调用你提供的函数。而是,它会缓存你提供的函数,以便 handleSubmit 本身不会更改,除非 productIdreferrer 已更改。这使你可以向下传递 handleSubmit 函数,而不必重新渲染 ShippingForm。在用户提交表单之前,你的代码不会运行。

    ¥**useCallback caches the function itself.** Unlike useMemo, it does not call the function you provide. Instead, it caches the function you provided so that handleSubmit itself doesn’t change unless productId or referrer has changed. This lets you pass the handleSubmit function down without unnecessarily re-rendering ShippingForm. Your code won’t run until the user submits the form.

如果你已经熟悉 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 之间的区别。

¥Read more about the difference between useMemo and useCallback.

深入研究

你应该在所有地方添加 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:

  • 你将它作为属性传递给封装在 memo 中的组件。如果该值未更改,你希望跳过重新渲染。记忆化让你的组件只有在依赖发生变化时才重新渲染。

    ¥You pass it as a prop to a component wrapped in memo. You want to skip re-rendering if the value hasn’t changed. Memoization lets your component re-render only if dependencies changed.

  • 你传递的函数稍后用作某些钩子的依赖。比如 useCallback 封装的另一个函数依赖它,或者你从 useEffect. 依赖这个函数。

    ¥The function you’re passing is later used as a dependency of some Hook. For example, another function wrapped in useCallback depends on it, or you depend on this function from 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.

在实践中,你可以通过遵循一些原则来避免大量记忆化:

¥In practice, you can make a lot of memoization unnecessary by following a few principles:

  1. 当一个组件在视觉上封装其他组件时,让它 接受 JSX 作为子级。 然后,如果封装器组件更新它自己的状态,React 知道它的子级不需要重新渲染。

    ¥When a component visually wraps other components, let it accept JSX as children. Then, if the wrapper component updates its own state, React knows that its children don’t need to re-render.

  2. 首选本地状态,除非必要,否则不要 提升状态。不要保持瞬时状态,例如表单,以及条目是否悬停在树的顶部或全局状态库中。

    ¥Prefer local state and don’t lift state up any further than necessary. Don’t keep transient state like forms and whether an item is hovered at the top of your tree or in a global state library.

  3. 保留你的 渲染逻辑纯粹。 如果重新渲染组件导致问题或产生一些明显的视觉伪像,那是你的组件中的错误!修复错误而不是添加记忆化。

    ¥Keep your rendering logic pure. If re-rendering a component causes a problem or produces some noticeable visual artifact, it’s a bug in your component! Fix the bug instead of adding memoization.

  4. 避免 更新状态的不必要的副作用。 React 应用中的大多数性能问题都是由副作用引起的更新链引起的,这些更新链导致组件反复渲染。

    ¥Avoid unnecessary Effects that update state. Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.

  5. 尝试以 从你的副作用中删除不必要的依赖。 例如,在副作用内部或组件外部移动某些对象或函数通常更简单,而不是记忆化。

    ¥Try to remove unnecessary dependencies from your Effects. For example, instead of memoization, it’s often simpler to move some object or a function inside an Effect or outside the component.

如果特定的交互仍然感觉滞后,使用 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 / 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 设为依赖并在内部读取它,而是将有关如何更新状态 (todos => [...todos, newTodo]) 的指令传递给 React。阅读有关更新函数的更多信息。

¥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

有时,你可能想从 副作用 内部调用函数:

¥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();
// ...

这就产生了一个问题。每个反应值都必须声明为副作用的依赖。 但是,如果你将 createOptions 声明为依赖,则会导致你的副作用不断重新连接到聊天室:

¥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
// ...

要解决这个问题,你可以将需要从副作用调用的函数封装到 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 函数在重新渲染之间相同。但是,最好不要依赖函数。将你的函数移入副作用中:

¥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详细了解如何删除副作用依赖。

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


优化自定义钩子

¥Optimizing a custom 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>
);
});

React 中文网 - 粤ICP备13048890号