被useCallback包裹的onSelect函数引发关联组件重复渲染问题
问题:useCallback包裹的函数引发未修改组件不必要重渲染
相关代码
状态与回调函数
const [descriptive, setDescriptive] = useState({}) const handleSelectionsCall = useCallback((event, inputLabel) => { const { target: { value }} = event; var newVal = typeof value === "string" ? value.split(",") : value; const lastItem = newVal[newVal.length -1] if (lastItem === 'Unselect') newVal = [] const update = descriptive[inputLabel]; const updatedDescriptive = { ...descriptive, [inputLabel]: { ...update, selected: newVal, error: newVal.length === 0 && ["Exchange", "Industry", "Sector"].includes(inputLabel) ? "At least one selection must be made" : "" } }; setDescriptive(updatedDescriptive); }, [descriptive, setDescriptive]);
三个CheckSelect组件调用
<div className={colSize}> <CheckSelect selected={descriptive.Sector.selected} allSelections={allSectors} allSelected={ descriptive.Sector.selected.length === allSectors.length } inputLabel="Sector" onSelect={handleSelectionsCall} labelFontSize="120%" extraOutlineSpace="fil" validationWarning={descriptive.Sector.error} size={smallWidth ? "small" : "medium"} /> </div> <div className={colSize}> <CheckSelect selected={descriptive.Industry.selected} allSelections={descriptive.Industry.allSelections} allSelected={ descriptive.Industry.selected.length === descriptive.Industry.allSelections.length } inputLabel="Industry" onSelect={handleSelectionsCall} labelFontSize="120%" extraOutlineSpace="fill" validationWarning={descriptive.Industry.error} size={smallWidth ? "small" : "medium"} /> </div> <div className={colSize}> <CheckSelect selected={descriptive.Exchange.selected} allSelections={exchanges} allSelected={descriptive.Exchange.selected.length === 2} inputLabel="Exchange" onSelect={handleSelectionsCall} labelFontSize="120%" extraOutlineSpace="filll" validationWarning={descriptive.Exchange.error} size={smallWidth ? "small" : "medium"} /> </div>
记忆化后的CheckSelect组件
export default memo(function CheckSelect({ allSelections, selected, onSelect, inputLabel='', labelFontSize, extraOutlineSpace='', validationWarning='test', size, allSelected=false }) { console.log('render ' + inputLabel) return ( <Select labelId="demo-multiple-checkbox-label" id="demo-multiple-checkbox" sx={selectFieldStyles(validationWarning)} multiple value={selected} onChange={e => onSelect(e, inputLabel)} input={<OutlinedInput label={inputLabel+extraOutlineSpace} />} renderValue={(selected) => { if (allSelected) return 'Any' return selected.join(', ') }} MenuProps={MenuProps} > {/* 渲染选项的map逻辑 */} </Select> ) })
问题现象
选中任意一个CheckSelect的选项时,另外两个未修改的组件也会触发重渲染,且它们的props(除了onSelect)并未发生变化。移除onSelect传递后,该现象消失。
原因分析与解决办法
核心原因
useCallback的依赖数组包含了descriptive这个state对象。每次选中选项时,你通过...descriptive创建了新的descriptive对象,导致useCallback的依赖项更新,生成新的handleSelectionsCall函数实例。
memo默认做浅比较,函数引用的变化会被判定为props更新,因此另外两个未修改的CheckSelect组件会触发重渲染。
解决方案
方案1:使用函数式更新,移除descriptive依赖
将setDescriptive改为函数式更新,直接使用React传入的最新状态,这样useCallback的依赖只需保留稳定的setDescriptive:
const handleSelectionsCall = useCallback((event, inputLabel) => { const { target: { value }} = event; var newVal = typeof value === "string" ? value.split(",") : value; const lastItem = newVal[newVal.length -1]; if (lastItem === 'Unselect') newVal = []; setDescriptive(prevDescriptive => { const update = prevDescriptive[inputLabel]; return { ...prevDescriptive, [inputLabel]: { ...update, selected: newVal, error: newVal.length === 0 && ["Exchange", "Industry", "Sector"].includes(inputLabel) ? "At least one selection must be made" : "" } }; }); }, [setDescriptive]);
setDescriptive是React在组件初始化时生成的稳定函数,不会随渲染改变,因此handleSelectionsCall的引用始终不变,未修改的CheckSelect组件不会触发不必要的重渲染。
方案2:自定义memo比较函数(不推荐)
如果不想修改回调函数逻辑,可以给memo传入自定义比较函数,忽略onSelect的变化:
export default memo(function CheckSelect(props) { // 组件原有逻辑 }, (prevProps, nextProps) => { // 手动比较除onSelect外的所有props return ( prevProps.selected === nextProps.selected && prevProps.allSelections === nextProps.allSelections && prevProps.allSelected === nextProps.allSelected && prevProps.inputLabel === nextProps.inputLabel && prevProps.labelFontSize === nextProps.labelFontSize && prevProps.extraOutlineSpace === nextProps.extraOutlineSpace && prevProps.validationWarning === nextProps.validationWarning && prevProps.size === nextProps.size ); });
此方案需手动维护比较逻辑,容易遗漏props更新,优先推荐方案1。
内容的提问来源于stack exchange,提问作者Logan McNulty
相关产品推荐
相关产品推荐

