Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React-useCallback Invalidates Too Often in Practice(React-useCallback 在实践中经常无效) #5

Open
bt2ee opened this issue Feb 27, 2021 · 1 comment
Labels
React React 完成 ✅ 翻译完成✅ 翻译 📖 翻译文章

Comments

@bt2ee
Copy link
Owner

bt2ee commented Feb 27, 2021

What is issue #14099 in React’s repo and how it affects you?

React 库中 #14099 issue 是什么?它如何影响你?

Why do we need useCallback in the first place?

In the official documentation of useCallback it says:

“This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders”

In the following example, PureHeavyComponent would re-render every single time that the Parent component is re-rendered although PureHeavyComponent is pure because previous props.onClick !== new props.onClick, because a new onClick function is created on every render of Parent.

首先我们为什么需要 useCallback?

useCallback 官方文档说明:

当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染的子组件时,它将非常有用。

在下面这个例子中,每次父组件重新渲染时 PureHeavyComponent 将会重新渲染,尽管 PureHeavyComponent 是个纯组件,但先前的 props.onClick !== new props.onClick,所以一个新的 onClick 事件会在父组件渲染时创建。

const PureHeavyComponent = React.memo(({onClick}) => {
  console.log('pure heavy component!');
  return (
    <div onClick={onClick}>
     //Many Other React elements are created here
    </div>
  );
});

const Parent = () => {
  console.log('parent!');
  const handleClick = () => console.log('hi!');
  return (
    <PureHeavyComponent onClick={handleClick} />
  );
};

// results:

// first render:
<Parent/>
// parent!
// pure heavy component!

// next renders:
<Parent/>
// parent!
// pure heavy component!

Since PureHeavyComponent is “heavy” we want it to re-render as less as possible. This is why we made it pure. But if we pass a new prop to it (onClick) on every render, we don’t achieve this goal.
This is where our useCallback comes into play
We wrap onClick with useCallback to ensure PureHeavyComponent renders only once.

由于 PureHeavyComponent 是“笨重的”,我们希望它尽可能的少重新渲染。这就是为什么我们让它成为纯组件。但是如果我们在每次渲染时传一个新的 prop 给他(onClick),我们就不能达到这个目的。
这是我们的 useCallback 发挥作用的地方。
我们使用 useCallback 包裹 onClick 方法来确保 PureHeavyComponent 只渲染一次。

const PureHeavyComponent = React.memo(({onClick}) => {
  console.log('pure heavy component!');
  return (
    <div onClick={onClick}>
     //Many Other React elements are created here
    </div>
  );
});

const Parent = () => {
  console.log('parent!');
  // returns a new handleClick function on every render of Parent
  const handleClick = React.useCallback(() => console.log('hi!'), []);
  return (
    <PureHeavyComponent onClick={handleClick} />
  );
};

// results:

// first render:
<Parent/>
// parent!
// pure heavy component!

// next renders:
<Parent/>
// parent!

useCallback caches (“memoizes”) the first function that was passed to it on the first render of Parent and always passes the same one to PureHeavyComponent.
Since PureHeavyComponent is pure, and since all of its props are equal this way, it doesn’t re-render anymore.

useCallback 缓存(“记忆”)父组件第一次渲染时传递给它的第一个函数并且总是传递同一个给 PureHeavyComponent
由于 PureHeavyComponent 是个纯组件,并且它的 props 遵从相同的方式,因此它不再重新渲染。

The Issue With useCallback

If any of the deps of useCallback change, handleClick is “invalidated” which means, it would no longer use the memoized value, but the new one that’s passed to it.
Consider the following code:

useCallback 的问题

如果任何 useCallback 的依赖变化了,handleClick 是“无效的”,这意味着,它不再使用缓存值,而使用传递的新值。

思考下面的代码:

const PureHeavyComponent = React.memo(({onClick}) => {
  console.log('pure heavy component!');
  return (
    <div onClick={onClick}>
     //Many Other React elements are created here
    </div>
  );
});

const Parent = () => {
  console.log('parent!');

  const [count, setCount] = React.useState(0);
  // returns a new instance of handleClick whenever count changes
  const handleClick = React.useCallback(() => console.log(`hi #${count}!`), [count]);
  
  return (
    <>
      <PureHeavyComponent onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>
        increase counter
      </button>
    </>
  );
};

// results:

// first render:
<Parent/>
// parent!
// pure heavy component!

// next renders as a result of clicking "increase counter":
<Parent/>
// parent!
// pure heavy component!

If Parent re-renders for any reason other then clicking on “increase count”, handleClick won’t be invalidated and everything works as expected.
But, whenever you click on the “increase count” button, since count changes, handleClick will be invalidated and PureHeavyComponent would re-render.
In this case since this render is “heavy”, it might cause a lag and a performance issue since it would slow down the application’s response time to a click.
This is exactly what the issue useCallback() invalidates too often in practice #14099 is all about.

如果父组件因为任何原因重新渲染而不是点击 “increase count” 按钮,handleClick 方法不会无效并且一切按照预期运行。
但是,当你点击 “increase count” 按钮,因为 count 变化,handleClick 将会无效并且 PureHeavyComponent 将会重新渲染。
这种情况下,由于渲染是 “笨重的”,它可能会引起延迟和性能问题因为它会降低应用对点击的响应时间。
这就是 #14099 阐述的 useCallback() 在实践中经常无效的全部原因。

Workarounds

Class Component
Remember the ‘prehistoric times’ when we used to use “Class Components”?

解决方案

类组件
记得我们使用类组件的“史前时代”吗?

const PureHeavyComponent = React.memo(({onClick}) => {
  console.log('pure heavy component!');
  return (
    <div onClick={onClick}>
     //Many Other React elements are created here
    </div>
  );
});

class Parent extends React.Component {
  state = {count: 0}

  // always the same function instance
  handleClick = () => {
    const {count} = this.state;
    console.log(`hi #${count}!`);
  }

  render() {
    return (
      <>
        <PureHeavyComponent onClick={this.handleClick} />
        <button onClick={() => this.setState({count: this.state.count + 1})}>increase counter</button>
      </>
    );
  }
};

This works just fine. this.handleClick is always the same function.

这样很好。 this.handleClick 始终是相同的函数。

useEventCallback

This hook and its drawback are also discussed in the official React docs.
useEventCallback does something clever. It saves the last function passed to it on useRef and exposes a memoized function that calls the saved function with all the relevant args.
But, this pattern might cause problems in the upcoming version of React with concurrent mode, so it is not recommended to be used unless you understand very well what’s the dangers involved in using it.

useEventCallback

这个 hook 和它的缺点都在官方文档中描述了。
useEventCallback 更聪明的处理一些事。它通过 useRef 保存最后一个传递的函数并且暴露一个函数,该函数使用所有有关参数来调用保存的函数。
但是,这个模式可能会在即将发布的并发模式下的 React 中引起问题。因此它不推荐使用除非你非常理解使用它会带来的危险。

/*
    Notice: This hook would be problematic in concurrent mode:
    https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
*/
function useEventCallback(fn, dependencies) {
  const ref = React.useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });
  React.useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);
  return React.useCallback((...args) => (0, ref.current)(...args), []);
}

A Possible Future Bug Fix

The React Core Team might improve the hook to always return the same function that would call the latest function that was passed to it. This way, wrapping a function with useCallback would never make pure components that use it re-render because of it.
It’s even possible to remove the second argument (deps) from the hook altogether and just keep on calling the latest function the hook received.
I believe this kind of solution would make the hook much more powerful, safe and easy to understand.

未来可能的 bug 修复

React 核心团队可能会改进这个 hook,来始终返回与调用传递给它的最新函数相同的函数。这样,用 useCallback 包装的函数将永远不会因此而使使用它的纯组件重新渲染。
甚至有可能完全从这个 hook 中删除第二个参数(deps),然后继续调用它接收到的最新函数。
我相信这种解决方案将使 hook 更加强大、安全和易于理解。

Summary

While there’s indeed an issue with useCallback, in most cases it works just fine. Recognizing the edge cases where it would be invalidated too often and using a workaround might improve your application’s performance in these edge cases.

概括

尽管useCallback确实存在问题,但在大多数情况下都可以正常工作。 认识到极端情况会使其无效,因此使用变通办法可能会提高您的应用程序在这些极端情况下的性能。

@bt2ee bt2ee changed the title React- useCallback Invalidates Too Often in Practice React-useCallback Invalidates Too Often in Practice(React-useCallback 在实践中经常无效) Feb 27, 2021
@bt2ee bt2ee added React React 完成 ✅ 翻译完成✅ 翻译 📖 翻译文章 labels Feb 27, 2021
@bt2ee
Copy link
Owner Author

bt2ee commented Feb 28, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
React React 完成 ✅ 翻译完成✅ 翻译 📖 翻译文章
Projects
None yet
Development

No branches or pull requests

1 participant