React中无法将state设置为字符串的问题排查
React useState设置字符串状态不生效?闭包与事件监听问题解析
问题根源
- 闭包陷阱:
switchDirection函数在startInterval中绑定为keydown回调时,捕获的是函数创建时刻的direction状态(初始值'what')。所以你在switchDirection里用console.log(direction)看到的永远是旧值,但实际上setDirection已经成功更新了状态——只是旧闭包无法访问最新状态。 - 重复绑定事件监听:每次点击按钮,都会给window新增一个keydown监听,多次点击后会导致
switchDirection被触发多次,干扰状态更新逻辑。 - 未清理定时器:
setInterval创建的定时器没有被清理,组件卸载或多次点击按钮会产生多个定时器实例,不仅浪费资源,还会导致蛇的移动逻辑混乱。
修复方案
1. 正确查看状态更新结果
不要在switchDirection里直接打印direction,改用useEffect监听direction变化,这样能获取到真实的最新状态:
useEffect(() => { console.log('当前方向:', direction); }, [direction]);
2. 避免重复绑定事件监听
在绑定新的keydown监听前,先移除旧的监听;同时在组件卸载时清理监听,防止内存泄漏:
// 在startInterval中先移除旧监听 window.removeEventListener('keydown', switchDirection); window.addEventListener('keydown', switchDirection); // 组件卸载时清理监听 useEffect(() => { return () => { window.removeEventListener('keydown', switchDirection); }; }, []);
3. 管理定时器生命周期
用useRef保存定时器ID,在重新开始游戏和组件卸载时清除旧定时器:
const timerRef = useRef(null); // 在startInterval中清除旧定时器 if (timerRef.current) { clearInterval(timerRef.current); } timerRef.current = setInterval(/* ... */, 1000); // 组件卸载时清除定时器 useEffect(() => { return () => { if (timerRef.current) { clearInterval(timerRef.current); } }; }, []);
4. 优化DOM操作(React最佳实践)
直接操作DOM不符合React的状态驱动理念,建议通过snake状态控制每个格子的样式:
{grids.map(grid => ( <div key={grid} style={{ backgroundColor: snake.includes(grid) ? 'black' : '' }} ></div> ))}
完整修复后的代码
import { useState, useEffect, useRef } from "react"; const Gameboard = () => { const [grids, setGrids] = useState([]); const [snake, setSnake] = useState([100, 101]); const [direction, setDirection] = useState('what'); const gameBoardRef = useRef(null); const timerRef = useRef(null); // 监听direction状态变化,查看最新值 useEffect(() => { console.log('当前方向:', direction); }, [direction]); const startInterval = () => { // 清除旧定时器 if (timerRef.current) { clearInterval(timerRef.current); } // 移除旧监听再绑定新监听 window.removeEventListener('keydown', switchDirection); window.addEventListener('keydown', switchDirection); timerRef.current = setInterval(() => { setSnake(prevState => { // 计算蛇的新位置 return prevState.map(piece => piece + 1); }); }, 1000); }; const switchDirection = (e) => { console.log(e.keyCode); switch(e.keyCode) { case 37: setDirection('left'); break; case 40: setDirection('down'); break; case 39: setDirection('right'); break; case 38: setDirection('up'); break; default: break; } }; useEffect(() => { // 初始化网格 const temp = Array.from({length: 196}, (_, i) => i); setGrids(temp); // 组件卸载时清理资源 return () => { if (timerRef.current) { clearInterval(timerRef.current); } window.removeEventListener('keydown', switchDirection); }; }, []); return ( <div> <button onClick={startInterval}>开始游戏</button> <div className="game-board" ref={gameBoardRef}> {grids.map(grid => ( <div key={grid} style={{ backgroundColor: snake.includes(grid) ? 'black' : '', width: '20px', height: '20px', border: '1px solid #eee', boxSizing: 'border-box' }} ></div> ))} </div> </div> ); }; export default Gameboard;
内容的提问来源于stack exchange,提问作者jr-kick
相关产品推荐
相关产品推荐

