正确使用 React hooks + WebSockets

Eug*_*aev 47 javascript websocket reactjs react-hooks

我需要连接到 WebSockets 服务器并记录它的消息。使用 React 类组件,我会将这个逻辑放在componentDidMount生命周期钩子中并愉快地继续,但我不确定如何使用钩子正确实现它。

这是我的第一次尝试。

import React, {useEffect} from 'react';

export default function AppWs() {
  useEffect(() => {
    let ws = new WebSocket('wss://ws.kraken.com/');
    ws.onopen = () => console.log('ws opened');
    ws.onclose = () => console.log('ws closed');

    ws.onmessage = e => {
      const message = JSON.parse(e.data);
      console.log('e', message);
    };

    return () => {
      ws.close();
    }
  }, []);

  return (
    <div>hooks + ws</div>
  )
}
Run Code Online (Sandbox Code Playgroud)

我向 中添加了连接和日志逻辑useEffect,提供了带有依赖项的空数组,并且一切正常。直到我需要添加pause状态以暂停日志记录。

export default function AppWs() {
  const [isPaused, setPause] = useState(false);

  useEffect(() => {
    let ws = new WebSocket('wss://ws.kraken.com/');
    ws.onopen = () => console.log('ws opened');
    ws.onclose = () => console.log('ws closed');

    ws.onmessage = e => {
      if (isPaused) return;
      const message = JSON.parse(e.data);
      console.log('e', message);
    };

    return () => {
      ws.close();
    }
  }, []);

  return (
    <div>
      <button onClick={() => setPause(!isPaused)}>{isPaused ? 'Resume' : 'Pause'}</button>
    </div>
  )
}
Run Code Online (Sandbox Code Playgroud)

ESLint 开始对我大喊大叫,我应该将isPaused状态作为依赖添加到useEffect.
嗯,好的,完成了。
但是我注意到每次单击按钮后都会重新连接到 WS 服务器。这显然不是我想要的。

我的下一次迭代是使用两个useEffects:一个用于连接,一个用于消息处理。

export default function AppWs() {
  const [isPaused, setPause] = useState(false);
  const [ws, setWs] = useState(null);

  useEffect(() => {
    const wsClient = new WebSocket('wss://ws.kraken.com/');
    wsClient.onopen = () => {
      console.log('ws opened');
      setWs(wsClient);
    };
    wsClient.onclose = () => console.log('ws closed');

    return () => {
      wsClient.close();
    }
  }, []);

  useEffect(() => {
    if (!ws) return;

    ws.onmessage = e => {
      if (isPaused) return;
      const message = JSON.parse(e.data);
      console.log('e', message);
    };
  }, [isPaused, ws]);

  return (
    <div>
      <button onClick={() => setPause(!isPaused)}>{isPaused ? 'Resume' : 'Pause'}</button>
    </div>
  )
}
Run Code Online (Sandbox Code Playgroud)

这按预期工作,但我有一种感觉,我错过了一些东西,这个任务可以更容易地解决,一个useEffect. 请帮助重构代码,让我相信我正在以正确的方式使用 React 钩子。谢谢!

Alv*_*aro 56

由于您只设置一次网络套接字,我认为更好的方法是使用 ref 而不是状态:

export default function AppWs() {
    const [isPaused, setPause] = useState(false);
    const ws = useRef(null);

    useEffect(() => {
        ws.current = new WebSocket("wss://ws.kraken.com/");
        ws.current.onopen = () => console.log("ws opened");
        ws.current.onclose = () => console.log("ws closed");

        return () => {
            ws.current.close();
        };
    }, []);

    useEffect(() => {
        if (!ws.current) return;

        ws.current.onmessage = e => {
            if (isPaused) return;
            const message = JSON.parse(e.data);
            console.log("e", message);
        };
    }, [isPaused]);

    return (
        <div>
            <button onClick={() => setPause(!isPaused)}>
                {isPaused ? "Resume" : "Pause"}
            </button>
        </div>
    );
}
Run Code Online (Sandbox Code Playgroud)

的顺序useEffect很重要。

  • 我在这里找到了自己问题的一个很好的答案:https://dmitripavlutin.com/react-useref-guide/ Refs 显然不仅仅适用于 DOM 元素。关键点是,它们在组件刷新之间持续存在,并且当值更改时不会触发组件刷新。非常适合这种场景 (9认同)
  • 当我使用此代码时,我注意到 websocket 会关闭,然后在每次收到消息后重新打开。有什么想法为什么要这样做吗? (4认同)
  • 顺便说一下,您应该将新创建的 websocket 存储到一个单独的变量中,并使用它进行清理。不是“ws.current”。其他一些代码可能会改变“ws.current”,并且旧的 websocket 实例不会被清理关闭。连接将保持打开状态并导致错误 (3认同)