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
相关产品推荐
相关产品推荐

