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

React实现按住方向键网格连续滚动及选中元素视口留存方案

定时器清理失败的核心原因

你当前方案的问题出在定时器引用的存储方式:React 函数组件每次重渲染时,普通变量或 state 中存储的定时器 id 会被重置,keyup 触发时无法拿到当前正在运行的定时器 id,就会出现清除失败、滚动停不下来的问题。

修复实现方案
  • 用 useRef 存储定时器 id,它的引用在组件全生命周期内稳定,不会随重渲染丢失
  • keydown 触发时先判断是否已有运行的定时器,避免重复创建多个定时器导致逻辑混乱
  • 选中单元格更新时调用 scrollIntoView 方法,保证选中元素始终在视口范围内
完整可运行代码
import { useState, useRef, useEffect } from 'react';

const Grid = () => {
  // 网格行列配置,可根据实际尺寸调整
  const ROW_COUNT = 3;
  const COL_COUNT = 3;
  // 当前选中的单元格坐标
  const [selected, setSelected] = useState({ row: 0, col: 0 });
  // 存储定时器引用
  const timerRef = useRef(null);

  // 移动选中单元格的逻辑
  const handleMove = (direction) => {
    setSelected(prev => {
      let newRow = prev.row;
      let newCol = prev.col;
      switch(direction) {
        case 'up':
          newRow = Math.max(0, prev.row - 1);
          break;
        case 'down':
          newRow = Math.min(ROW_COUNT - 1, prev.row + 1);
          break;
        case 'left':
          newCol = Math.max(0, prev.col - 1);
          break;
        case 'right':
          newCol = Math.min(COL_COUNT - 1, prev.col + 1);
          break;
      }
      return { row: newRow, col: newCol };
    });
  };

  useEffect(() => {
    // 选中元素滚动到视口
    const activeCell = document.getElementById(`cell-${selected.row}-${selected.col}`);
    if (activeCell) {
      activeCell.scrollIntoView({
        behavior: 'smooth',
        block: 'nearest',
        inline: 'nearest'
      });
    }

    const handleKeyDown = (e) => {
      // 避免重复创建定时器
      if (timerRef.current) return;
      let direction;
      switch(e.key) {
        case 'ArrowUp':
          direction = 'up';
          break;
        case 'ArrowDown':
          direction = 'down';
          break;
        case 'ArrowLeft':
          direction = 'left';
          break;
        case 'ArrowRight':
          direction = 'right';
          break;
        default:
          return;
      }
      // 第一次触发先移动一次
      handleMove(direction);
      // 开启定时器持续移动,150ms间隔可根据手感调整
      timerRef.current = setInterval(() => {
        handleMove(direction);
      }, 150);
      // 阻止默认滚动行为
      e.preventDefault();
    };

    const handleKeyUp = (e) => {
      if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
        // 清除定时器
        clearInterval(timerRef.current);
        timerRef.current = null;
        e.preventDefault();
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);

    // 组件卸载时清除定时器,避免内存泄漏
    return () => {
      clearInterval(timerRef.current);
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  }, [selected]);

  // 渲染网格
  const renderGrid = () => {
    const rows = [];
    for (let i = 0; i < ROW_COUNT; i++) {
      const cells = [];
      for (let j = 0; j < COL_COUNT; j++) {
        const isActive = selected.row === i && selected.col === j;
        cells.push(
          <div
            id={`cell-${i}-${j}`}
            key={`cell-${i}-${j}`}
            style={{
              width: '80px',
              height: '80px',
              border: '1px solid #ccc',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: isActive ? '#e0f2fe' : '#fff',
              outline: isActive ? '2px solid #0ea5e9' : 'none'
            }}
          >
            {i * COL_COUNT + j + 1}
          </div>
        );
      }
      rows.push(
        <div key={`row-${i}`} style={{ display: 'flex' }}>
          {cells}
        </div>
      );
    }
    return rows;
  };

  return (
    <div 
      id='grid' 
      style={{
        display: 'flex',
        flexDirection: 'column',
        // 网格尺寸大时开启滚动
        overflow: 'auto',
        maxHeight: '400px',
        maxWidth: '400px'
      }}
    >
      {renderGrid()}
    </div>
  );
};

export default Grid;
配置说明
  • 调整 setInterval 的第二个参数可以修改滚动速度,数值越小滚动越快
  • 如果需要仅在网格容器内滚动而不影响页面整体滚动,可以给网格容器固定宽高并设置 overflow: auto,scrollIntoView 会自动适配容器滚动
  • 选中态的样式可以根据设计需求自行调整,当前示例用了蓝色背景和边框做高亮

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 10:39:04