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

React函数组件keydown事件监听器添加及状态更新异常排查

问题分析与解决方案

失效原因拆解

  1. 空依赖useEffect绑定document事件:
    闭包陷阱导致updateActiveCellId始终捕获初始的activeCellId值(42),每次触发事件都只会把状态设为43,后续更新无效;且未清理事件监听,存在内存泄漏风险。

  2. 网格div直接绑定onKeyDown无效:
    div默认不具备焦点能力,无法接收键盘事件,必须通过tabIndex属性将其设置为可聚焦元素。

  3. useRef绑定div事件无效:
    同样存在div无焦点的问题,且闭包导致activeCellId无法获取最新值,同时未做事件清理。


解决方案

以下是两种可行的实现方案,推荐第一种React原生事件绑定方式:

方案一:网格容器绑定事件(推荐)

通过给网格容器添加tabIndex使其可聚焦,配合函数式更新状态避免闭包问题,同时自动聚焦容器:

import { useState, useRef, useEffect } from 'react';

const GridCells: React.FC = () => {
  const gridArray = [...Array(100).keys()];
  const color = [
    "bg-slate-50",
    "bg-slate-100",
    "bg-slate-200",
    "bg-slate-300",
    "bg-slate-400",
    "bg-slate-500",
    "bg-slate-600",
    "bg-slate-700",
    "bg-slate-800",
    "bg-slate-900",
  ];

  const [activeCellId, setActiveCellId] = useState(42);
  const gridRef = useRef<HTMLDivElement>(null);

  // 组件挂载时自动聚焦网格容器
  useEffect(() => {
    gridRef.current?.focus();
  }, []);

  const updateActiveCellId = (e: React.KeyboardEvent) => {
    e.preventDefault(); // 阻止页面滚动等默认行为
    switch(e.key) {
      case 'ArrowUp':
        setActiveCellId(prev => Math.max(prev - 10, 0)); // 防止越界到负数
        break;
      case 'ArrowDown':
        setActiveCellId(prev => Math.min(prev + 10, 99)); // 防止越界超过99
        break;
      case 'ArrowLeft':
        setActiveCellId(prev => prev % 10 !== 0 ? prev - 1 : prev); // 行首无法左移
        break;
      case 'ArrowRight':
        setActiveCellId(prev => prev % 10 !== 9 ? prev + 1 : prev); // 行尾无法右移
        break;
      default:
        return;
    }
  };

  return (
    <div
      ref={gridRef}
      className="grid-rows-10 grid grid-cols-10 gap-0.5 outline-none" // 移除默认聚焦边框(可选)
      tabIndex={0}
      onKeyDown={updateActiveCellId}
    >
      {gridArray.map((value, id) => {
        const colorId = Math.floor(id / 10);
        return (
          <div
            key={id}
            className={`h-10 w-10 ${color[colorId]} ${id === activeCellId ? 'scale-125 bg-yellow-400' : ''}`}
          >
            {id}
          </div>
        );
      })}
    </div>
  );
};

export default GridCells;

方案二:全局document绑定事件

如果需要全局监听键盘事件(无需聚焦网格),可使用useCallback包裹事件函数并处理依赖:

import { useState, useEffect, useCallback } from 'react';

const GridCells: React.FC = () => {
  const gridArray = [...Array(100).keys()];
  const color = [
    "bg-slate-50",
    "bg-slate-100",
    "bg-slate-200",
    "bg-slate-300",
    "bg-slate-400",
    "bg-slate-500",
    "bg-slate-600",
    "bg-slate-700",
    "bg-slate-800",
    "bg-slate-900",
  ];

  const [activeCellId, setActiveCellId] = useState(42);

  // useCallback确保函数依赖更新时重新生成
  const updateActiveCellId = useCallback((e: KeyboardEvent) => {
    e.preventDefault();
    switch(e.key) {
      case 'ArrowUp':
        setActiveCellId(prev => Math.max(prev - 10, 0));
        break;
      case 'ArrowDown':
        setActiveCellId(prev => Math.min(prev + 10, 99));
        break;
      case 'ArrowLeft':
        setActiveCellId(prev => prev % 10 !== 0 ? prev - 1 : prev);
        break;
      case 'ArrowRight':
        setActiveCellId(prev => prev % 10 !== 9 ? prev + 1 : prev);
        break;
      default:
        return;
    }
  }, []);

  useEffect(() => {
    document.addEventListener('keydown', updateActiveCellId);
    // 组件卸载时清理事件监听
    return () => {
      document.removeEventListener('keydown', updateActiveCellId);
    };
  }, [updateActiveCellId]);

  return (
    <div className="grid-rows-10 grid grid-cols-10 gap-0.5">
      {gridArray.map((value, id) => {
        const colorId = Math.floor(id / 10);
        return (
          <div
            key={id}
            className={`h-10 w-10 ${color[colorId]} ${id === activeCellId ? 'scale-125 bg-yellow-400' : ''}`}
          >
            {id}
          </div>
        );
      })}
    </div>
  );
};

export default GridCells;

核心优化点

  • tabIndex={0}:让网格容器成为可聚焦元素,能够接收键盘事件。
  • 函数式更新状态:setActiveCellId(prev => ...)确保每次更新基于最新状态值,避开闭包陷阱。
  • 边界处理:通过Math.max/Math.min和取余运算,保证activeCellId始终在0-99的合法范围内。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 14:25:23