You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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上,和子组件无关。

修复步骤:

  1. 用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]);
  1. 更新useEffect的依赖项数组,加入keyDown,确保状态更新时重新绑定事件:
useEffect(() => {
  window.addEventListener('resize', updateSize);
  window.addEventListener('keydown', keyDown);
  return () => {
      window.removeEventListener('resize', updateSize);
      window.removeEventListener('keydown', keyDown);
  }
}, [keyDown, updateSize]); // 注意updateSize也应该用useCallback包裹,避免不必要的重绑定
  1. 修复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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 14:39:51