假设我的组件很慢,记住它很有意义,例如
const SlowButton = ({onClick}) => {
// make some heat
const value = (function f(n) { return n < 2 ? Math.random() : f(n-1) + f(n-2)})(32)|0;
return <button onClick={() => onClick(value)}>{value}</button>
}
const MemoButton = React.memo(SlowButton);
如果在类似这样的组件中使用MemoButton:
const Counter = () => {
const [clicks, setClicks ] = useState(0);
const handleClick = () => {
setClicks(clicks + 1);
}
return <div>
<div>{clicks}</div>
<MemoButton onClick={handleClick} />
</div>
}
然后,MemoButton仍然每次都重新渲染,因为
onClick
属性是每个渲染的新函数。解决这个问题很容易:const Counter2 = () => {
const [clicks, setClicks] = useState(0);
const handleClick = useCallback(() => {
setClicks(c => c + 1);
},[]);
return <div>
<div>{clicks}</div>
<MemoButton onClick={handleClick} />
</div>
}
上面的方法工作正常,但组件更复杂,效果不佳:
const CounterGroup = () => {
const numButtons = 3;
const [message, setMessage] = useState('button X clicked');
const handleClick = (val, idx) => setMessage(`button ${idx} clicked: ${val}`);
return <div>
<div>{message}</div>
{Array(numButtons).fill(0).map((_, i) =>
<MemoButton key={i} onClick={(v) => handleClick(v,i)} />)
}
</div>
}
在上面的代码中,
(v) => handleClick(i,v)
始终将是一个新的函数引用。有没有好的技术来防止更改每个渲染?一种可能性是忽略对“ on ...”道具的更改,但这只会带来新的问题:
const compareValuePropsOnly = (prev, next) => Object.entries(prev).every(
([k, v]) => k.substr(0, 2) === "on" || v === next[k]
);
const MemoOnlyValsButton = React.memo(SlowButton, compareValuePropsOnly);
这是一个codeandbox版本:
https://codesandbox.io/s/memoization-function-reference-changes-9c1fy
最佳答案
一种解决方案是让您的SlowButton
传递i
值,而不是从循环中获取它,并记住handleClick
const SlowButton = ({onClick, i}) => {
// make some heat
const value = (function f(n) { return n < 2 ? Math.random() : f(n-1) + f(n-2)})(32)|0;
return <button onClick={() => onClick(value, i)}>{value}</button>
}
const MemoButton = React.memo(SlowButton);
const CounterGroup = () => {
const numButtons = 3;
const [message, setMessage] = useState('button X clicked');
const handleClick = React.useCallback((val, idx) => setMessage(`button ${idx} clicked: ${val}`), []);
return <div>
<div>{message}</div>
{Array(numButtons).fill(0).map((_, i) =>
<MemoButton key={i} i={i} onClick={handleClick} />)
}
</div>
}
另一种方法是在
React.memo
中排除“ onClick”道具(由于它是事件处理程序,因此不会影响组件的外观)。 const MemoButton = React.memo(SlowButton, (props1, props2) => {
// assume that SlowButton have some props that affect it's UI
// we don't compare onClick because it won't affect UI
return props1.someUIProps === props2.someUIProps;
})
或者,您可以使用
useEventCallback
挂钩记录功能。在这种情况下,您需要在CounterGroup
和MemoButton
之间创建一个组件const useEventCallback = (callback) => {
// store latest callback
const ref = useRef(callback);
useEffect(() => ref.current = callback);
// memoize the callback to maintain its identity
return useCallback((...args) => ref.current(...args), []);
}
const FastButton = ({onClick} => {
// FastButton will be re-rendered multiple times, but since memoOnClick have same identity
// on sub sequence re-renders, MemoButton should not be re-rendered
const memoOnClick = useEventCallback(onClick);
return <MemoButton onClick={memoOnClick} />
});
const CounterGroup = () => {
const numButtons = 3;
const [message, setMessage] = useState('button X clicked');
const handleClick = (val, idx) => setMessage(`button ${idx} clicked: ${val}`);
return <div>
<div>{message}</div>
{Array(numButtons).fill(0).map((_, i) =>
<FastButton key={i} onClick={(v) => handleClick(v,i)} />)
}
</div>
}