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

React+Socket.io射击游戏:useState创建子弹DOM后querySelector返回null

问题

我用HTML、React和Socket.io开发在线射击游戏,当其他用户射击时,我能拿到数据并在本地渲染子弹(div元素),但刚创建完子弹就用querySelector获取它时,总是返回null。之前遇到DOM元素访问问题我会用useRef,但这次觉得useRef不太适用。

我的实现步骤:

  1. 用useState声明bulletsDivEnemy存储其他用户的子弹元素,渲染到DOM:
const [bulletsDivEnemy, setBulletsDivEnemy] = useState([]);
// ... 其他代码
// 渲染到DOM
{bulletsDivEnemy}
  1. 用useLayoutEffect监听Socket.io的receive_shoot事件,触发shotsOfEnemy函数:
useLayoutEffect(() => {
  socket.on("receive_shoot", (data) => {
    shotsOfEnemy(data)
  }) 
}, [socket])
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 01:14:59