使用 React Hooks 重新连接 Web 套接字

Sus*_*van 2 websocket reactjs react-hooks

我想与服务器建立网络套接字连接。如果连接关闭,则在 5 秒后重新连接。我正在使用 React Hooks,到目前为止实现了这一目标

import React, { useRef, useState, useEffect } from 'react';`
import './App.css';
function App() {
 const wsClient = useRef(null);
 const [wsState, setWsState] = useState(true)
 useEffect(() => {
    wsClient.current = new WebSocket(url);
    console.log("Trying to open ws");
    setWsState(true)
    
    wsClient.current.onopen = () => {
      console.log('ws opened');
      wsClient.current.send('{"type" : "hello"}')
    };
    wsClient.current.onclose = (event) => {
      // Parse event code and log
      setTimeout(() => {setWsState(false)}, 5000)
      console.log('ws closed');
    }
    wsClient.current.onmessage = ((event) => {
      // DO YOUR JOB
    })
    return () => {
      console.log('ws closed');
      wsClient.current.close();
    }
  }, [wsState]);
  return (
      <div className="App">
        <Header />
        <MainBody />
      </div>
  );
}

Run Code Online (Sandbox Code Playgroud)

当它无法与服务器连接时,这会导致重试次数呈指数增长,如果我删除setTimeout并使用 simplesetState则它可以正常工作。我无法理解这个问题,并建议实现我的目标的最佳做法是什么。

Jos*_*hua 10

我不相信效果是最好的地方。如果它是应用程序级别的,在它自己的模块中实现它可能更简单,并在需要的地方引入它。

尽管如此,要使其工作,您应该考虑管理两个独立的生命周期:组件生命周期和 websocket 生命周期。为了让它如你所愿地工作,你必须确保一个的每个状态变化与另一个的状态变化保持一致。

首先,请记住,每次数组中的依赖项更改时,您的效果都会运行。因此,在您的示例中,每次设置wsState.

要记住的另一件事是每次wsState更改时都会调用您的清理函数,您在效果中执行了两次(将其设置为true打开和false关闭)。这意味着当你创建一个新的套接字并且连接失败时,关闭事件会触发,并且它会排队状态更改。

每次尝试连接时,它都会设置wsState为true(将重新运行效果排入队列),尝试连接但连接失败,最后设置另一个超时,将状态更新为false。但是,不是在效果再次运行之前,尝试将状态设置为 true,等等。

要解决此问题,请从效果生命周期开始。你的效果应该什么时候运行?什么时候应该清理?一些想法:

  1. 该效果应在第一次渲染期间运行一次,但在后续渲染期间不应运行
  2. WebSocket断开连接时应该清理效果
  3. 该效果应在超时后重新运行,触发重新连接

这对组件意味着什么?您不想将 WS 状态包含为依赖项。但是,您确实需要状态来触发它在超时后重新运行。

这是它的样子:

import React, { useRef, useState, useEffect } from 'react';

const URL = 'ws://localhost:8888';

export default function App() {

  const clientRef = useRef(null);
  const [waitingToReconnect, setWaitingToReconnect] = useState(null);
  const [messages, setMessages] = useState([]);
  const [isOpen, setIsOpen] = useState(false);

  function addMessage(message) {
    setMessages([...messages, message]); 
  }

  useEffect(() => {

    if (waitingToReconnect) {
      return;
    }

    // Only set up the websocket once
    if (!clientRef.current) {
      const client = new WebSocket(URL);
      clientRef.current = client;

      window.client = client;

      client.onerror = (e) => console.error(e);

      client.onopen = () => {
        setIsOpen(true);
        console.log('ws opened');
        client.send('ping');
      };

      client.onclose = () => {

        if (clientRef.current) {
          // Connection failed
          console.log('ws closed by server');
        } else {
          // Cleanup initiated from app side, can return here, to not attempt a reconnect
          console.log('ws closed by app component unmount');
          return;
        }

        if (waitingToReconnect) {
          return;
        };

        // Parse event code and log
        setIsOpen(false);
        console.log('ws closed');

        // Setting this will trigger a re-run of the effect,
        // cleaning up the current websocket, but not setting
        // up a new one right away
        setWaitingToReconnect(true);

        // This will trigger another re-run, and because it is false,
        // the socket will be set up again
        setTimeout(() => setWaitingToReconnect(null), 5000);
      };

      client.onmessage = message => {
        console.log('received message', message);
        addMessage(`received '${message.data}'`);
      };


      return () => {

        console.log('Cleanup');
        // Dereference, so it will set up next time
        clientRef.current = null;

        client.close();
      }
    }

  }, [waitingToReconnect]);

  return (
    <div>
      <h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
      {waitingToReconnect && <p>Reconnecting momentarily...</p>}
      {messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,跟踪连接状态,但不在 useEffect 依赖项中。waitingForReconnect是,不过。它在连接关闭时设置,一段时间后取消设置,以触发重新连接尝试。

清理也会触发关闭,因此我们需要onClose通过查看客户端是否已取消引用来区分。

正如您所看到的,这种方法相当复杂,它将 WS 生命周期与组件生命周期联系起来(如果您在应用程序级别这样做,这在技术上是可以的)。

然而,一个主要的警告是很容易遇到陈旧的闭包问题。例如,addMessage可以访问局部变量消息,但由于addMessage没有作为依赖项传入,因此每次运行效果不能调用它两次,否则它会覆盖最后一条消息。(它本身不是覆盖;它实际上只是用旧的“陈旧”值覆盖状态messages,并与新值连接。调用它十次,你只会看到最后一个值。)

因此,您可以添加addMessage依赖项,但随后您将在每次渲染时断开并重新连接 websocket。您可以摆脱addMessages, 并将该逻辑移动到效果中,但是每次更新messages数组时它都会重新运行(比每次渲染的频率低,但仍然太频繁)。

因此,回到完整的循环,我建议在应用程序生命周期之外设置您的客户端。您可以使用自定义钩子来处理传入的消息,或者直接在效果中处理它们。

这是一个例子:

import React, { useRef, useState, useEffect } from 'react';

const URL = 'ws://localhost:8888';

export default function App() {

  const clientRef = useRef(null);
  const [waitingToReconnect, setWaitingToReconnect] = useState(null);
  const [messages, setMessages] = useState([]);
  const [isOpen, setIsOpen] = useState(false);

  function addMessage(message) {
    setMessages([...messages, message]); 
  }

  useEffect(() => {

    if (waitingToReconnect) {
      return;
    }

    // Only set up the websocket once
    if (!clientRef.current) {
      const client = new WebSocket(URL);
      clientRef.current = client;

      window.client = client;

      client.onerror = (e) => console.error(e);

      client.onopen = () => {
        setIsOpen(true);
        console.log('ws opened');
        client.send('ping');
      };

      client.onclose = () => {

        if (clientRef.current) {
          // Connection failed
          console.log('ws closed by server');
        } else {
          // Cleanup initiated from app side, can return here, to not attempt a reconnect
          console.log('ws closed by app component unmount');
          return;
        }

        if (waitingToReconnect) {
          return;
        };

        // Parse event code and log
        setIsOpen(false);
        console.log('ws closed');

        // Setting this will trigger a re-run of the effect,
        // cleaning up the current websocket, but not setting
        // up a new one right away
        setWaitingToReconnect(true);

        // This will trigger another re-run, and because it is false,
        // the socket will be set up again
        setTimeout(() => setWaitingToReconnect(null), 5000);
      };

      client.onmessage = message => {
        console.log('received message', message);
        addMessage(`received '${message.data}'`);
      };


      return () => {

        console.log('Cleanup');
        // Dereference, so it will set up next time
        clientRef.current = null;

        client.close();
      }
    }

  }, [waitingToReconnect]);

  return (
    <div>
      <h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
      {waitingToReconnect && <p>Reconnecting momentarily...</p>}
      {messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)