我们应该在 React 功能组件的每个函数处理程序中使用 useCallback

Posted

技术标签:

【中文标题】我们应该在 React 功能组件的每个函数处理程序中使用 useCallback【英文标题】:Should we use useCallback in every function handler in React Functional Components 【发布时间】:2021-01-15 22:44:54 【问题描述】:

假设我们有这样的组件

const Example = () => 
  const [counter, setCounter] = useState(0);
  
  const increment = () => setCounter(counter => counter + 1); 
  return (
    <div>
      <Button onClick=increment />
      
      <div>counter</div>
    </div>
  );

当我将onClick 处理程序作为箭头函数 传递时,我的eslint 会抛出警告:

error    JSX props should not use arrow functions        react/jsx-no-bind

正如我从这篇文章的回答中读到的:https://***.com/questions/36677733/why-shouldnt-jsx-props-use-arrow-functions-or-bind#:~:text=Why%20you%20shouldn't%20use,previous%20function%20is%20garbage%20collected。

简短的回答是因为每次都重新创建箭头函数,这会损害性能。 这篇文章提出的一种解决方案是用一个空数组包裹在 useCallback 钩子中。而当我改成这个时, eslint 警告就真的消失了。

const Example = () => 
  const [counter, setCounter] = useState(0);
  
  const increment = useCallback(() => setCounter(counter => counter + 1), []);
  
  return (
    <div>
      <Button onClick=increment />
      
      <div>counter</div>
    </div>
  );

但是,也有另一种观点认为,过度使用 useCallback 最终会因为 useCallback 的开销而降低性能。一个例子在这里:https://kentcdodds.com/blog/usememo-and-usecallback

这让我很困惑?所以对于函数式组件,在处理内联函数处理程序时,我应该只写箭头函数(忽略 eslint)还是 always 将其包装在 useCallback ???

【问题讨论】:

eslint 错误react/jsx-no-bind,在我看来,最有可能是 React 组件类,而不是功能组件。不过,我可能错了。 IMO 它给你一个错误,因为它无法区分有状态和功能组件之间的区别 就我个人而言,我会从 eslint 设置中删除 react/jsx-no-bind 并记住在我所有的有状态类中使用自动绑定器 like this one。我很容易记住,因为我的 IDE 允许我拥有模板,所以每当我创建一个新的 React 类时,我只使用包含自动绑定器的模板。 【参考方案1】:

在我看来,useCallback 不是为了性能。我想不出定义一个函数真的很昂贵的任何原因。 与useMemo 不同,useCallback 只是记忆函数而不实际执行。

那么我们应该什么时候使用它呢?

主要用例是防止不必要地重新运行函数。重新定义一个函数没有问题,但是在每次状态更新时重新运行它是错误的,而且通常很危险。

TL 博士;仅当函数需要位于 useEffect 的依赖数组内时才使用 useCallback

我现在能想到的有两种情况:

    例如,一个函数是异步的,我们需要在任何依赖项发生变化时运行它:
const [data, setData] = useState([]);
const [filter, setFilter] = useState();

const fetchData = useCallback(async () => 
  const response = await fetchApi(filter);
  setData(response.data);
, [filter]);

useEffect(() => 
  fetchData();
, [fetchData]);

(如果函数不是异步的,我们可以直接使用useEffect而不使用useCallback

但是,当它只通过用户交互运行时,不需要用useCallback 包装它:

const [data, setData] = useState([]);
const [filter, setFilter] = useState();

const fetchData = async () => 
  const response = await fetchApi(filter);
  setData(response.data);
;

return (
  <button onClick=fetchData>Fetch Data</button>
);
    当您应该将函数 prop 传递给第 3 方组件时:
const onAwesomeLibarayLoaded = useCallback(() => 
  doSomething(state1, state2);
, [state1, state2]);

<AwesomeLibrary 
  onLoad=onAwesomeLibarayLoaded
/>

因为AwesomeLibrary 组件可能会通过onLoad 函数执行类似于示例1 的操作:

const AwesomeLibarary = (onLoad) => 
  useEffect(() => 
    // do something
    onLoad();
  , [onLoad]);
;

如果您确定它不在useEffect 内,那么即使您不使用useCallback 也可以。

【讨论】:

“在我看来,useCallback 不是为了性能。我想不出任何理由定义一个函数真的很昂贵”,好吧,你的观点是错误的。你读过docs吗? “这在将回调传递给依赖引用相等以防止不必要的渲染的优化子组件时很有用”。主要用例 不是 防止不必要地重新运行函数,而是避免重新渲染。 other answer 解释得很好。【参考方案2】:

简短的回答是因为每次都重新创建箭头函数,这会损害性能。

这是一个常见的误解。每次无论哪种方式都会重新创建箭头函数(尽管useCallback 后续的可能会立即被丢弃)。 useCallback 所做的是使您使用回调的子组件在被记忆时不会重新渲染。

让我们先看看误解。考虑useCallback 调用:

const increment = useCallback(() => setCounter(counter => counter + 1), []);

执行如下:

    计算第一个参数() =&gt; setCounter(counter =&gt; counter + 1)创建一个函数

    计算第二个参数[],创建一个数组

    用这两个参数调用useCallback,返回一个函数

与不使用useCallback的情况比较:

const increment = () => setCounter(counter => counter + 1);

这要简单得多:创建函数。然后就不必执行上面的#2 和#3。

让我们继续讨论useCallback 的实际用途。我们看一下回调在哪里使用:

<Button onClick=increment />

现在,假设 Button 被记忆为 React.memo 或类似的。如果increment 每次你的组件渲染都会改变,那么Button 必须在每次你的组件改变时重新渲染;它不能在渲染之间重用。但是如果increment在两次渲染之间是稳定的(因为你使用了一个空数组的useCallback),调用Button的记忆结果可以被重复使用,它不必再次调用。

这是一个例子:

const  useState, useCallback  = React;

const Button = React.memo(function Button(onClick, children) 
    console.log("Button called");
    return <button onClick=onClick>children</button>;
);

function ComponentA() 
    console.log("ComponentA called");
    const [count, setCount] = useState(0);
    // Note: Safe to use the closed-over `count` here if `count `updates are
    // triggered by clicks or similar events that definitely render, since
    // the `count` that `increment` closes over won't be stale.
    const increment = () => setCount(count + 1);
    return (
        <div>
            count
            <Button onClick=increment>+</Button>
        </div>
    );


function ComponentB() 
    console.log("ComponentB called");
    const [count, setCount] = useState(0);
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    const increment = useCallback(
        () => setCount(count => count + 1),
        []
    );
    return (
        <div>
            count
            <Button onClick=increment>+</Button>
        </div>
    );


ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

请注意,单击ComponentA 中的按钮总是会再次调用Button,但单击ComponentB 中的按钮则不会。

你想什么时候这样做?这在很大程度上取决于您,但是当您的组件状态以不影响increment 内容的方式频繁更改并且因此不影响Button 如果@ 987654347@ 在渲染时必须做大量工作。 Button 可能不会,但其他子组件可能会。

例如,如果您使用count 作为按钮的文本,我之前示例中的useCallback 可能毫无意义,因为这意味着Button 无论如何都必须重新渲染:

const  useState, useCallback  = React;

const Button = React.memo(function Button(onClick, children) 
    console.log("Button called");
    return <button onClick=onClick>children</button>;
);

function ComponentA() 
    console.log("ComponentA called");
    const [count, setCount] = useState(0);
    // Note: Safe to use the closed-over `count` here if `count `updates are
    // triggered by clicks or similar events that definitely render, since
    // the `count` that `increment` closes over won't be stale.
    const increment = () => setCount(count + 1);
    return (
        <div>
            <Button onClick=increment>count</Button>
        </div>
    );


function ComponentB() 
    console.log("ComponentB called");
    const [count, setCount] = useState(0);
    // Note: Can't use `count` in `increment`, need the callback form because
    // the `count` the first `increment` closes over *will* be slate after
    // the next render
    const increment = useCallback(
        () => setCount(count => count + 1),
        []
    );
    return (
        <div>
            <Button onClick=increment>count</Button>
        </div>
    );


ReactDOM.render(
    <div>
        A:
        <ComponentA />
        B:
        <ComponentB />
    </div>,
    document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

还要注意useCallback 不是免费的,它会影响回调中的代码。查看示例中ComponentAComponentB 中的回调中的代码。 ComponentA(不使用useCallback)可以使用count 的值(在限制内!)() =&gt; setCount(count + 1)。但是ComponentB 中的那个总是必须使用setter 的回调形式() =&gt; setCount(count =&gt; count + 1)。那是因为如果你继续使用你创建的第一个 increment,它关闭的 count 将是陈旧的 - 你会看到计数变为 1,但不会再进一步​​。

【讨论】:

以上是关于我们应该在 React 功能组件的每个函数处理程序中使用 useCallback的主要内容,如果未能解决你的问题,请参考以下文章

React 功能组件 - 对处理程序使用内联函数会影响性能?

事件处理:基于功能的组件与基于类的组件

使用React组件管理数据

如何在React中处理组件交互?

如何在纯函数式 React 组件中处理事件

将函数作为道具传递给 Typescript React 功能组件