React+Socket.io射击游戏:useState创建子弹DOM后querySelector返回null
问题
我用HTML、React和Socket.io开发在线射击游戏,当其他用户射击时,我能拿到数据并在本地渲染子弹(div元素),但刚创建完子弹就用querySelector获取它时,总是返回null。之前遇到DOM元素访问问题我会用useRef,但这次觉得useRef不太适用。
我的实现步骤:
- 用
useState声明bulletsDivEnemy存储其他用户的子弹元素,渲染到DOM:
const [bulletsDivEnemy, setBulletsDivEnemy] = useState([]); // ... 其他代码 // 渲染到DOM {bulletsDivEnemy}
- 用
useLayoutEffect监听Socket.io的receive_shoot事件,触发shotsOfEnemy函数:
useLayoutEffect(() => { socket.on("receive_shoot", (data) => { shotsOfEnemy(data) }) }, [socket])
- 在
shotsOfEnemy里更新bulletsDivEnemy添加新子弹,但获取元素时出错:
function shotsOfEnemy(data){ setBulletsDivEnemy((prevDivs) => { return [ ...prevDivs, <div key={prevDivs.length} className={` h-1 absolute z-50 origin-right bullet-parent`} style={{ transform: `rotate(${data.bulletPath}deg)`, // 修正原代码语法错误 top: `${data.gunTop + 4}px `, right: `${data.gunXDistance}px`, }} > {/* 子弹元素 */} <div className="bullet" id={`bullet-ene${prevDivs.length}`} // 替换原代码的bulletsDivLength,避免状态不同步 ></div> </div>, ]; }); const bullet = document.querySelector(`#bullet-ene${bulletsDivLength}`); // bullet 总是 NULL!!! }
解决方案
核心原因
React的setState是异步操作,调用setBulletsDivEnemy后,DOM不会立刻更新。你紧接着用querySelector去拿元素,此时新子弹还没渲染到页面上,自然返回null。
可行解决办法
方法1:用useEffect监听子弹列表变化
等bulletsDivEnemy更新、DOM渲染完成后,再操作新子弹元素:
function shotsOfEnemy(data){ setBulletsDivEnemy((prevDivs) => { const newBulletId = `bullet-ene${prevDivs.length}`; // 临时存储新子弹ID,方便后续获取 sessionStorage.setItem('latestBulletId', newBulletId); return [ ...prevDivs, <div key={prevDivs.length} className={` h-1 absolute z-50 origin-right bullet-parent`} style={{ transform: `rotate(${data.bulletPath}deg)`, top: `${data.gunTop + 4}px`, right: `${data.gunXDistance}px`, }} > <div className="bullet" id={newBulletId}></div> </div>, ]; }); } // 监听子弹列表变化,DOM更新后再获取元素 useEffect(() => { const latestBulletId = sessionStorage.getItem('latestBulletId'); if (latestBulletId) { const bullet = document.querySelector(`#${latestBulletId}`); if (bullet) { // 这里写你的动画逻辑,比如添加类名触发CSS动画 bullet.classList.add('bullet-fly'); sessionStorage.removeItem('latestBulletId'); // 用完清除 } } }, [bulletsDivEnemy]);
方法2:用CSS动画代替手动DOM操作
直接给子弹元素添加带动画的类名,React渲染时自动触发动画,无需手动获取元素:
function shotsOfEnemy(data){ setBulletsDivEnemy((prevDivs) => { return [ ...prevDivs, <div key={prevDivs.length} className={` h-1 absolute z-50 origin-right bullet-parent`} style={{ transform: `rotate(${data.bulletPath}deg)`, top: `${data.gunTop + 4}px`, right: `${data.gunXDistance}px`, }} > {/* 直接添加动画类名 */} <div className="bullet bullet-fly" id={`bullet-ene${prevDivs.length}`}></div> </div>, ]; }); }
对应的CSS动画定义:
.bullet { width: 20px; height: 4px; background-color: yellow; position: absolute; } .bullet-fly { animation: bulletMove 0.5s linear forwards; } @keyframes bulletMove { to { transform: translateX(-100vw); /* 根据游戏逻辑调整移动距离 */ } }
方法3:用ref数组跟踪子弹元素(适合精细控制场景)
如果必须用ref,可以维护一个ref数组,每次添加子弹时创建新ref并保存:
const bulletRefs = useRef([]); function shotsOfEnemy(data){ setBulletsDivEnemy((prevDivs) => { const newIndex = prevDivs.length; // 创建新ref并加入数组 bulletRefs.current[newIndex] = React.createRef(); return [ ...prevDivs, <div key={newIndex} className={` h-1 absolute z-50 origin-right bullet-parent`} style={{ transform: `rotate(${data.bulletPath}deg)`, top: `${data.gunTop + 4}px`, right: `${data.gunXDistance}px`, }} > <div className="bullet" ref={bulletRefs.current[newIndex]} id={`bullet-ene${newIndex}`} ></div> </div>, ]; }); } // 监听子弹列表变化,用ref获取元素 useEffect(() => { const latestRef = bulletRefs.current[bulletRefs.current.length - 1]; if (latestRef && latestRef.current) { // 操作子弹元素 latestRef.current.classList.add('bullet-fly'); } }, [bulletsDivEnemy]);
额外提示
- 原代码中
transform: rotate(${data.bulletPath)deg)存在语法错误,已修正为${data.bulletPath}deg; - 用
prevDivs.length作为key和ID比外部变量更可靠,避免状态不同步问题。
内容的提问来源于stack exchange,提问作者J. Villasmil
相关产品推荐
相关产品推荐

