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

非严格模式下React中useEffect多次调用问题排查

问题:StompJS客户端初始化函数重复调用,useRef状态未正确保留

我希望仅执行一次connect函数初始化StompJS客户端,但该函数被调用了4次,浏览器报错:

VM1867 bundle.js:41684 WebSocket connection to 'ws://localhost:3000/chat/251/ios42gwd/websocket' failed: WebSocket is closed before the connection is established.

第4次尝试后成功连接WebSocket。我理解useRef会在组件重渲染间保留值,但实际clientInit.current四次均为false,请问哪里出错了?

原始代码

function App() {
  const [userData, setUserData] = useState({
    userId: null,
    name: "",
    connected: false,
  });

  const [connected, setConnected] = useState(false);
  const [stompClient, setStompClient] = useState(null);

  const clientInit = useRef(false);

  //should be called only once? but called 4 times. getting clientInit.current= false 4 times 
 too 
  const connect = () => {         console.log("inside connect");
    console.log("====================================");
    console.log(clientInit.current);
    console.log("====================================");
    clientInit.current = true;

    const socket = new SockJS("/chat");
    const tempstompClient = Stomp.over(socket);
    tempstompClient.connect(
      { "client-id": userData.userId },
      () => {
        setConnected(true);
        setStompClient(tempstompClient);
      },
      (er) => {
        console.error(er);
      }
    );
  };

  const registerUser = (e) => {
    e.preventDefault();
    setUserData({ ...userData, connected: true });
  };

  const handleUser = (event) => {
    const { value } = event.target;
    const uuid = uuidv4();
    setUserData({ ...userData, name: value, userId: uuid });
  };
  useEffect(() => {
    console.log("====================================");
    console.log("calling connect:", clientInit.current);
    console.log("====================================");
    if(!clientInit)
      connect();

    return () => {
      console.log("Doing stuff inside cleaning up", stompClient);
      if (stompClient) {
        console.log("====================================");
        console.log("disconnecting", stompClient);
        console.log("====================================");
        stompClient.disconnect();
      }
    };
  }, []);

  return userData.connected ? (
    <Chatbox
      userData={userData}
      stompClient={stompClient}
      connected={connected}
    />
  ) : (
    <Register handleUser={handleUser} handleSubmit={registerUser} />
  );
}

export default App;

错误原因分析

  1. useRef判断逻辑错误:在useEffect中写的是if(!clientInit),但clientInit是一个Ref对象,永远不会为假值,导致每次useEffect执行都会调用connect。正确的判断应该是!clientInit.current。
  2. 闭包导致的状态捕获问题:useEffect的依赖数组为空,清理函数中捕获的stompClient是初始的null,无法获取后续更新的客户端实例,可能导致无法正确断开连接。
  3. 连接时机过早:初始时userData.userId为null,此时发起连接会携带无效的client-id,可能导致连接失败,后续组件重渲染又重复触发连接。

修复方案及代码

function App() {
  const [userData, setUserData] = useState({
    userId: null,
    name: "",
    connected: false,
  });

  const [connected, setConnected] = useState(false);
  const stompClientRef = useRef(null); // 改用useRef存储客户端,避免闭包问题
  const clientInit = useRef(false);

  const connect = () => {
    console.log("inside connect");
    console.log("====================================");
    console.log(clientInit.current);
    console.log("====================================");
    
    // 双重保险,防止重复执行
    if (clientInit.current) return;
    clientInit.current = true;

    const socket = new SockJS("/chat");
    const tempstompClient = Stomp.over(socket);
    tempstompClient.connect(
      { "client-id": userData.userId },
      () => {
        setConnected(true);
        stompClientRef.current = tempstompClient;
      },
      (er) => {
        console.error(er);
        clientInit.current = false; // 连接失败时重置状态,允许后续重试
      }
    );
  };

  const registerUser = (e) => {
    e.preventDefault();
    setUserData({ ...userData, connected: true });
  };

  const handleUser = (event) => {
    const { value } = event.target;
    const uuid = uuidv4();
    setUserData({ ...userData, name: value, userId: uuid });
  };

  useEffect(() => {
    console.log("====================================");
    console.log("calling connect:", clientInit.current);
    console.log("====================================");
    
    // 仅在未初始化且有用户ID时发起连接
    if (!clientInit.current && userData.userId) {
      connect();
    }

    return () => {
      console.log("Doing stuff inside cleaning up", stompClientRef.current);
      if (stompClientRef.current) {
        console.log("====================================");
        console.log("disconnecting", stompClientRef.current);
        console.log("====================================");
        stompClientRef.current.disconnect();
        stompClientRef.current = null;
        clientInit.current = false; // 重置状态,下次挂载可重新连接
      }
    };
  }, [userData.userId]); // 依赖userData.userId,确保用户ID生成后再连接

  return userData.connected ? (
    <Chatbox
      userData={userData}
      stompClient={stompClientRef.current}
      connected={connected}
    />
  ) : (
    <Register handleUser={handleUser} handleSubmit={registerUser} />
  );
}

export default App;

关键修改点

  • 将stompClient从useState改为useRef,确保清理函数能获取最新的客户端实例
  • 在connect函数开头增加重复执行判断,双重保险防止多次连接
  • 修正useEffect中的判断条件为!clientInit.current,并增加userData.userId校验,确保连接时机合理
  • 将useEffect的依赖数组改为[userData.userId],保证用户ID生成后才发起连接
  • 清理函数中重置clientInit.current和stompClientRef.current,避免下次挂载时状态混乱

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 17:56:00