React中useMemo绑定列表事件遇状态捕获问题的解决方案咨询
问题
需要实现一个可追踪选中项的选择器,要求任意选项元素触发onMouseLeave事件时能访问当前选中项(例如console.log当前选中项)。使用useMemo根据props.options生成渲染列表,代码如下:
const [pickedElement, setPickedElement] = useState(""); const move_to_picked = () => { console.log(pickedElement); }; const optionsList = useMemo(() => { let answer = []; for (let item of options) answer.push( <div key={item} onClick={() => setPickedElement(item)} onMouseLeave={move_to_picked} > <p>{item}</p> </div> ); return answer; }, [options]);
触发move_to_picked函数时,打印的是pickedElement的初始状态。经排查,选中新选项后move_to_picked会生成新版本,但未在optionsList中重新绑定。将pickedElement加入useMemo依赖后功能正常,但担心仅因选中新选项就遍历整个数组的性能问题,咨询:
- 是否有办法不重新绑定
move_to_picked,从而避免遍历数组? - 当前方案是否有误,正确的实现方案是什么?
解答
1. 避免重新绑定move_to_picked的方案
可以用useRef保存pickedElement的最新值,让move_to_picked始终能访问到当前最新状态,同时不需要将pickedElement加入useMemo依赖,也就不会触发数组遍历重新生成列表:
const [pickedElement, setPickedElement] = useState(""); const pickedRef = useRef(pickedElement); // 同步ref与state的最新值 useEffect(() => { pickedRef.current = pickedElement; }, [pickedElement]); const move_to_picked = () => { // 访问ref.current获取最新选中项 console.log(pickedRef.current); }; const optionsList = useMemo(() => { let answer = []; for (let item of options) answer.push( <div key={item} onClick={() => setPickedElement(item)} onMouseLeave={move_to_picked} > <p>{item}</p> </div> ); return answer; }, [options]); // 仅依赖options,选中项变化不会重新生成列表
原理是useRef的current属性是可变的,不会受闭包捕获旧值的影响,move_to_picked始终指向同一个函数实例,不需要重新绑定到列表元素上。
2. 当前方案的问题与正确实现
当前方案的核心问题是**useMemo的依赖项不全**:optionsList中用到了move_to_picked,而move_to_picked依赖pickedElement,但pickedElement未被加入useMemo的依赖数组,导致pickedElement更新时,optionsList不会重新生成,列表元素绑定的仍是旧版本的move_to_picked,闭包捕获的是旧的pickedElement值。
正确的实现有两种选择:
选择一:补全依赖项(推荐,除非列表项数量极大)
直接将pickedElement加入useMemo的依赖数组,这样pickedElement更新时会重新生成列表。实际上React的useMemo和列表遍历的开销非常小,只要options的数量不是成千上万级,完全不需要担心性能问题,过早优化反而会增加代码复杂度:const optionsList = useMemo(() => { // ... 原有逻辑 }, [options, pickedElement]);选择二:使用
useRef方案(适合列表项数量极大的场景)
也就是上面第一问的解法,通过useRef规避闭包问题,避免列表重新生成,适合列表项数量特别多、重新生成会带来明显性能开销的场景。
内容的提问来源于stack exchange,提问作者Cross

