React组件为何在设置状态与keydown事件间丢失状态?
问题:按下箭头键触发keydown函数时,所有状态变量未填充
我在页面中放置了楼层平面图图片,通过getScale()方法计算物理尺寸与屏幕尺寸的缩放比例。当缩放比例更新后,加载办公桌并根据该比例在平面图上定位,同时将办公桌存入Ref数组,方便访问对应的div元素调整边框。
目前点击办公桌时,边框调整功能正常,但按下箭头键触发keydown函数时,所有状态变量均未被填充。这是为什么?
我猜测:是不是因为keydown事件从子组件触发,导致获取的是子组件的状态而非父组件(函数所在组件)的状态?
父组件代码片段:
const component = () => { const mapRef = useRef(); const desksRef = useRef([]); const [mapScale, setMapScale] = useState(); const [desks, setDesks] = useState({ past: [], present: [], future: [] }); const [currentDesk, setCurrentDesk] = useState(-1); const [currentMap, setCurrentMap] = useState(-1); const [maps, setMaps] = useState({ maps: [] }); const [editMode, setEditMode] = useState(false); useEffect(() => { window.addEventListener('resize', updateSize); window.addEventListener('keydown', keyDown); return () => { window.removeEventListener('resize', updateSize); window.removeEventListener('keydown', keyDown); } }, []) useEffect(() => { const desks = buildDesks(); setDesks({ ...desks, present: maps.maps[currentMap]?.desks }); setDeskComponents(desks); }, [editMode]) useEffect(() => { const desks = buildDesks(); setDesks({ present: maps.maps[currentMap]?.desks }); setDeskComponents(desks); }, [mapScale]); const getScale = () => { const site = maps.maps[currentMap]; // 下拉选择的ID if (!site) return; let map = mapRef.current; let rect = map?.getBoundingClientRect(); let mapWidth = rect.width; let mapHeight = rect.height; let scaleW = (mapWidth && site?.wMM) ? (mapWidth / site.wMM) : 1; let scaleH = (mapHeight && site?.hMM) ? (mapHeight / site.hMM) : 1; setMapScale({ height: scaleH, width: scaleW }); } const DeskFns = { Update_Desklist: setDesks, Update_Desk_Data: updateDesk, Set_UserImage: setUserImage, Set_Current: setCurrentDesk, Get_Current: () => desks.present[currentDesk].id, Press_Key: keyDown, //redraw: redraw, } const buildDesk = (desk, index) => { const res = <Desk key={desk.id} desk={desk} isEditing={editMode} desks={desks.present} scale={mapScale} deskTypes={deskTypes} currentDesk={currentDesk} fns={DeskFns} ref={el => desksRef.current[index] = el} index={index} onKeyDown={keyDown} /> return res; } const buildDesks = () => { //getScale(); try { let res = maps.maps[currentMap]?.desks.map((desk, index) => buildDesk(desk, index)); //let res = desks.present.map(desk => buildDesk(desk); return res } catch (error) { console.error('Error building desks:', error); return null; } }; function keyDown(e) { if (currentDesk > -1 && editMode == true) { e.preventDefault(); var key = e.key; const desk = desks.present[currentDesk]; if (editMode == true) { switch (key) { case 'ArrowLeft': // 左箭头 desk.x -= 5 / mapScale.width; break; case 'ArrowUp': // 上箭头 desk.y -= 5 / mapScale.height; break; case 'ArrowRight': // 右箭头 desk.x += 5 / mapScale.width; break; case 'ArrowDown': // 下箭头 desk.y += 5 / mapScale.height; break; default: break; } updateDesk(desk); } } } return <React.Fragment>{deskComponents}</React.Fragment> }
子组件Desk代码片段:
export const Desk = forwardRef(({ desk, isEditing, scale, deskTypes, fns, index }, ref, onKeyDown) => { const imgRef = useRef(null); const [scl,] = useState(scale); // 之前用来修正问题,现在不需要了 const [rotation, setRotation] = useState(desk?.rotation ?? 0); const [size, setSize] = useState(() => { let deskImg = null; var dImg = deskTypes?.find(dsk => dsk.deskType === desk.deskType); let top = parseInt(desk.y * scl.height); const left = parseInt(desk.x * scl.width); let width = 0; let height = 0; try { if (dImg) { deskImg = dImg.deskImage; width = parseInt(dImg.wMM * scl.width); height = parseInt(dImg.hMM * scl.height); } let imgStyle = { boxSizing: "border-box", width: `${width}px`, height: `${height}px`, } const url = `data:image/jpeg;base64,${deskImg}`; return { url: url, style: imgStyle, left: left ?? 0, top: top ?? 0 }; } catch (ex) { console.log(ex); } return { left: left, top: top }; }); const handleImageClick = (e) => { e.stopPropagation(); fns.Set_Current(index); }; useImperativeHandle(ref, () => ({ test: () => { console.log('test'); }, img: () => imgRef.current })); return ( // 包裹在<Draggable container...>中 <div > <img ref={ref} alt={`X=${size.left} Y=${size.top} ID=${desk.id} Rotation=${rotation}`} src={size.url} style={{ ...size.style, position: 'absolute', transform: `rotate(${rotation}deg)`, cursor: 'move', }} onClick={(e) => handleImageClick(e)} // 确保点击事件被处理 /> </div> ) });
问题原因及解决方案
问题核心是闭包陷阱:你在useEffect中绑定keydown事件时,依赖项数组为空,导致keyDown函数捕获的是组件首次渲染时的状态快照。后续状态更新后,keyDown仍使用旧的状态值,所以看起来状态变量“未填充”。你猜测的子组件触发事件的问题不成立——事件绑定在window上,和子组件无关。
修复步骤:
- 用
useCallback包裹keyDown函数,确保它能捕获最新状态:
const keyDown = useCallback((e) => { if (currentDesk > -1 && editMode) { e.preventDefault(); const key = e.key; const desk = desks.present[currentDesk]; if (!desk || !mapScale) return; // 增加边界判断,避免报错 switch (key) { case 'ArrowLeft': desk.x -= 5 / mapScale.width; break; case 'ArrowUp': desk.y -= 5 / mapScale.height; break; case 'ArrowRight': desk.x += 5 / mapScale.width; break; case 'ArrowDown': desk.y += 5 / mapScale.height; break; default: break; } updateDesk(desk); } }, [currentDesk, editMode, desks.present, mapScale, updateDesk]);
- 更新
useEffect的依赖项数组,加入keyDown,确保状态更新时重新绑定事件:
useEffect(() => { window.addEventListener('resize', updateSize); window.addEventListener('keydown', keyDown); return () => { window.removeEventListener('resize', updateSize); window.removeEventListener('keydown', keyDown); } }, [keyDown, updateSize]); // 注意updateSize也应该用useCallback包裹,避免不必要的重绑定
- 修复
DeskFns中的Get_Current闭包问题:
const getCurrentDeskId = useCallback(() => { if (currentDesk === -1 || !desks.present[currentDesk]) return null; return desks.present[currentDesk].id; }, [currentDesk, desks.present]); const DeskFns = { // ...其他属性 Get_Current: getCurrentDeskId, }
修改后,keydown事件就能获取到最新的状态变量了。
内容的提问来源于stack exchange,提问作者Geoff
相关产品推荐
相关产品推荐

