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

React/Next.js键盘按下事件监听触发异常问题排查

React/Next.js图片画廊模态框键盘导航异常问题

问题背景

我在React/Next.js中构建Lightroom风格的图片画廊组件:点击画廊图片通过React Portal打开模态框,关闭按钮和鼠标点击左右箭头的导航功能正常。画廊数据如下:

[{src: "/bear.jpg", width: '600', height: '750', alt: 'bear'},
{src: "/duck.jpg", width: '600', height: '750', alt: 'duck'},
{src: "/wombat.jpg", width: '600', height: '750', alt: 'wombat'},
{src: "/salamander.jpg", width: '600', height: '750', alt: 'salamander'},
{src: "/goose.jpg", width: '600', height: '750', alt: 'goose'},
{src: "/mouse.jpg", width: '600', height: '750', alt: 'mouse'}]

通过主组件传入的imgNumber(useState状态)跟踪当前图片索引(范围0-5),鼠标点击导航可实现循环切换,功能正常。但添加键盘事件监听后,ESC键关闭模态框功能正常,左右箭头导航却始终使用旧的索引值(比如左箭头对应4,右箭头对应5),无法正确更新。

问题根源

问题出在useEffect的依赖数组为空:事件监听器keyDownHandler仅在组件挂载时绑定一次,闭包捕获了初始的imgNumber值。后续imgNumber更新时,监听器内的函数仍引用旧状态值,导致键盘导航计算的是过时的索引。

解决方案

方案1:更新useEffect依赖数组

将imgNumber加入useEffect的依赖数组,确保每次状态更新时重新绑定事件监听器,让函数获取最新状态值:

useEffect(() => {
  document.addEventListener("keydown", keyDownHandler, false);

  return () => {
    document.removeEventListener("keydown", keyDownHandler, false);
  };
}, [imgNumber]); // 加入imgNumber作为依赖

如果navLeftHandle和navRightHandle是组件内定义的函数,建议用useCallback包裹,避免不必要的重绑定:

const navLeftHandle = useCallback(() => {
  setImgNumber(prev => prev <= 0 ? gallery.length - 1 : prev - 1);
}, [gallery.length, setImgNumber]);

const navRightHandle = useCallback(() => {
  setImgNumber(prev => prev >= gallery.length - 1 ? 0 : prev + 1);
}, [gallery.length, setImgNumber]);

// 更新useEffect依赖
useEffect(() => {
  document.addEventListener("keydown", keyDownHandler, false);
  return () => document.removeEventListener("keydown", keyDownHandler, false);
}, [navLeftHandle, navRightHandle]);

方案2:使用函数式更新状态

设置状态时采用函数式更新,直接基于前一次状态计算新值,无需依赖当前imgNumber变量,即使闭包捕获旧状态也能正确获取最新值:

function navLeftHandle() {
  setImgNumber(prev => {
    const newNum = prev <= 0 ? gallery.length - 1 : prev - 1;
    console.log("New Image Number: ", newNum);
    return newNum;
  });
}

function navRightHandle() {
  setImgNumber(prev => prev >= gallery.length - 1 ? 0 : prev + 1);
}

这种方式无需修改useEffect的依赖数组,因为函数式更新直接读取React内部保存的最新状态。

方案3:内联事件处理逻辑到useEffect中

将keyDownHandler直接写在useEffect内部,确保每次依赖变化时,函数能获取最新的状态和上下文:

useEffect(() => {
  const keyDownHandler = (e) => {
    if (e.repeat) return;

    if (e.key === "Escape") {
      e.preventDefault();
      setViewModal(false);
    } else if (e.key === "ArrowLeft") {
      e.preventDefault();
      setImgNumber(prev => prev <= 0 ? gallery.length - 1 : prev - 1);
    } else if (e.key === "ArrowRight") {
      e.preventDefault();
      setImgNumber(prev => prev >= gallery.length - 1 ? 0 : prev + 1);
    }
  };

  document.addEventListener("keydown", keyDownHandler, false);
  return () => document.removeEventListener("keydown", keyDownHandler, false);
}, [gallery.length, setViewModal, setImgNumber]);

核心逻辑说明

React组件内的函数会在每次渲染时重新创建,闭包会捕获当前渲染周期的状态值。当事件监听器仅在挂载时绑定(依赖数组为空),后续状态更新不会改变监听器内部的状态引用,从而导致使用旧值。通过更新依赖数组、使用函数式更新或内联处理逻辑,均可规避这个闭包陷阱。

内容的提问来源于stack exchange,提问作者Josh Walter

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 15:18:03