You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
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:
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.
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.
/*
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.
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.
The text was updated successfully, but these errors were encountered:
bt2ee
changed the title
React- useCallback Invalidates Too Often in Practice
React-useCallback Invalidates Too Often in Practice(React-useCallback 在实践中经常无效)
Feb 27, 2021
React 库中 #14099 issue 是什么?它如何影响你?
首先我们为什么需要 useCallback?
useCallback 官方文档说明:
在下面这个例子中,每次父组件重新渲染时
PureHeavyComponent
将会重新渲染,尽管PureHeavyComponent
是个纯组件,但先前的 props.onClick !== new props.onClick,所以一个新的 onClick 事件会在父组件渲染时创建。由于
PureHeavyComponent
是“笨重的”,我们希望它尽可能的少重新渲染。这就是为什么我们让它成为纯组件。但是如果我们在每次渲染时传一个新的 prop 给他(onClick),我们就不能达到这个目的。这是我们的 useCallback 发挥作用的地方。
我们使用 useCallback 包裹 onClick 方法来确保
PureHeavyComponent
只渲染一次。useCallback 缓存(“记忆”)父组件第一次渲染时传递给它的第一个函数并且总是传递同一个给
PureHeavyComponent
。由于 PureHeavyComponent 是个纯组件,并且它的 props 遵从相同的方式,因此它不再重新渲染。
useCallback 的问题
如果任何 useCallback 的依赖变化了,handleClick 是“无效的”,这意味着,它不再使用缓存值,而使用传递的新值。
思考下面的代码:
如果父组件因为任何原因重新渲染而不是点击 “increase count” 按钮,handleClick 方法不会无效并且一切按照预期运行。
但是,当你点击 “increase count” 按钮,因为 count 变化,handleClick 将会无效并且
PureHeavyComponent
将会重新渲染。这种情况下,由于渲染是 “笨重的”,它可能会引起延迟和性能问题因为它会降低应用对点击的响应时间。
这就是 #14099 阐述的 useCallback() 在实践中经常无效的全部原因。
解决方案
类组件
记得我们使用类组件的“史前时代”吗?
这样很好。 this.handleClick 始终是相同的函数。
useEventCallback
这个 hook 和它的缺点都在官方文档中描述了。
useEventCallback 更聪明的处理一些事。它通过 useRef 保存最后一个传递的函数并且暴露一个函数,该函数使用所有有关参数来调用保存的函数。
但是,这个模式可能会在即将发布的并发模式下的 React 中引起问题。因此它不推荐使用除非你非常理解使用它会带来的危险。
未来可能的 bug 修复
React 核心团队可能会改进这个 hook,来始终返回与调用传递给它的最新函数相同的函数。这样,用 useCallback 包装的函数将永远不会因此而使使用它的纯组件重新渲染。
甚至有可能完全从这个 hook 中删除第二个参数(deps),然后继续调用它接收到的最新函数。
我相信这种解决方案将使 hook 更加强大、安全和易于理解。
概括
尽管useCallback确实存在问题,但在大多数情况下都可以正常工作。 认识到极端情况会使其无效,因此使用变通办法可能会提高您的应用程序在这些极端情况下的性能。
The text was updated successfully, but these errors were encountered: