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

React+Socket.io多人游戏开发:useEffect中socket.on回调无法访问最新状态/属性及重复执行问题排查

问题分析与解决方案

一、初始问题:scoreList和isWinnerList显示为空的原因

你遇到的第一个核心问题是闭包陷阱:当你在空依赖数组的useEffect里绑定socket事件时,回调函数会捕获组件初始渲染时的scoreList和isWinnerList值(也就是空对象)。之后即使这些状态更新,回调里的变量依然是最初的空对象,所以console.log会显示空,基于旧值的状态更新逻辑自然也会出错。

虽然你对setIsWinnerList用了函数式更新(prev => ...),这部分是正确的,但在判断isWinnerList[user.name] === false时,你还是用了闭包中的旧值,这会导致逻辑判断失效。


二、修改后updateScore回调执行多次的原因

当你把useEffect的依赖改成[isWinnerList]后,每次isWinnerList更新,这个useEffect就会重新执行,而你每次执行都调用socket.on('updateScore', ...)添加一个新的事件监听器,却没有清理旧的。这样一来,同一个updateScore事件会触发多个回调,自然就会执行多次。

如果把所有依赖(scoreList、setScoreList、socket)都加上,每次这些值变化(比如scoreList更新),useEffect就会重新绑定监听器,导致监听器越积越多,最终出现近乎无限循环的情况。


三、正确的解决方案

我们需要解决两个核心问题:获取最新的状态值和避免重复绑定socket事件,具体步骤如下:

1. 使用useRef保存最新的状态

通过useRef可以保存最新的scoreList和isWinnerList,这样在socket回调里就能访问到当前的状态值,不受闭包限制。

2. 在useEffect中添加清理函数

每次useEffect执行前,先解绑之前的socket事件监听器,避免重复绑定。

3. 合理设置useEffect的依赖

只把稳定的依赖(比如socket、setScoreList)加入数组,同时用useRef跟踪状态变化,避免因为状态更新导致不必要的useEffect重复执行。

修改后的代码示例:

import React, { useContext, useEffect, useState, useRef } from 'react';
import { SocketContext } from '../../context/socket';
import PlayerBoard from './PlayerBoard/PlayerBoard';
import './Scoreboard.css';

const Scoreboard = ({users, scoreList, setScoreList}) => {
 const socket = useContext(SocketContext);
 const [isWinnerList, setIsWinnerList] = useState({});
 // 用ref保存最新的状态,避免闭包陷阱
 const scoreListRef = useRef(scoreList);
 const isWinnerListRef = useRef(isWinnerList);

 // 同步ref与状态,确保ref始终指向最新值
 useEffect(() => {
  scoreListRef.current = scoreList;
 }, [scoreList]);

 useEffect(() => {
  isWinnerListRef.current = isWinnerList;
 }, [isWinnerList]);

 useEffect(() => {
  // 定义独立的事件处理函数,方便后续解绑
  const handleRightAnswer = ({ user }) => {
   // 用函数式更新确保基于最新状态修改
   setIsWinnerList(prev => ({...prev, [user]: true}));
  };

  const handleUpdateScore = ({users}) => {
   // 从ref获取最新的状态值
   const currentWinners = isWinnerListRef.current;
   const currentScores = scoreListRef.current;

   // 批量收集需要更新的分数,减少setState调用次数
   const updatedScores = {};
   for(const user of users){
    if(currentWinners[user.name] === false){
     console.log("updating score");
     updatedScores[user.name] = (currentScores[user.name] || 0) + 3;
    }
   }

   // 只有当有分数更新时才调用setState
   if(Object.keys(updatedScores).length > 0){
    setScoreList(prev => ({...prev, ...updatedScores}));
   }
  };

  // 绑定socket事件
  socket.on('rightAnswer', handleRightAnswer);
  socket.on('updateScore', handleUpdateScore);

  // 清理函数:在组件卸载或依赖变化时解绑事件
  return () => {
   socket.off('rightAnswer', handleRightAnswer);
   socket.off('updateScore', handleUpdateScore);
  };
 }, [socket, setScoreList]); // 仅依赖稳定的socket和setScoreList

 useEffect(() => {
  if(!users) return;
  // 批量初始化isWinnerList,避免多次调用setState
  const initialWinners = {};
  users.forEach(user => {
   if(!(user.name in isWinnerList)){
    initialWinners[user.name] = false;
   }
  });
  if(Object.keys(initialWinners).length > 0){
   setIsWinnerList(prev => ({...prev, ...initialWinners}));
  }
 }, [users, isWinnerList]);

 return (
  <div className='outerScoreboard'>
   {users ? (
    <div className='innerScoreboard'>
     {users.map(({ name }) => (
      <div key={name} className='scoreboardItem'>
       <PlayerBoard 
        name={name} 
        isWinner={isWinnerList[name]} 
        score={scoreList[name] || 0} 
       />
      </div>
     ))}
    </div>
   ) : <div> no users </div>}
  </div>
 )
};

export default Scoreboard;

关键说明

  • useRef的作用:scoreListRef和isWinnerListRef会始终指向最新的状态值,这样在socket回调里就能获取到当前的分数和赢家状态,不会受闭包限制。
  • 清理函数:return () => { ... }会在组件卸载或者useEffect依赖变化时执行,解绑之前的事件监听器,彻底避免重复绑定导致的多次回调。
  • 批量更新状态:在handleUpdateScore里先收集所有需要更新的分数,再一次性调用setScoreList,减少不必要的组件重渲染。
  • 依赖数组:只把socket和setScoreList加入依赖,这两个值在组件生命周期内是稳定的(setScoreList是useState返回的函数,不会变化),所以不会导致useEffect频繁执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 20:27:33