websocket React无效的钩子调用:如何避免一个嵌套钩子?

xuo3flqw  于 2023-10-20  发布在  React
关注(0)|答案(3)|浏览(169)

如何在useEffect内部建立连接,同时使用自定义钩子装饰该连接?自定义钩子不允许在useEffect内部运行,并且在渲染期间不允许ref.current。什么是用React友好的接口 Package 连接的正确方法?
我相信在React中建立WebSocket连接的正确方法是使用useEffect钩子,沿着

const connection = useRef(null);

useEffect(() => {
  const ws = new WebSocket(socketUrl);
  connection.current = ws;

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

以类似的方式,我设想创建一个RTCDataConnection,它遵循与.send('...')opencloseerrormessage事件相同的连接接口。
我正在寻找一种方法来为这些连接提供一个类似React的接口。我在考虑像下面这样的自定义useConnection钩子,它接受一个JavaScript连接对象,并返回两个React状态变量lastMessagereadyState,以及一个方法sendMessage

export function useConnection(connection) {
  const [lastMessage, setLastMessage] = useState(null);
  const [readyState, setReadyState] = useState(null);

  const onReadyStateChange = useCallback(
    (e) => {
      console.info(e);
      setReadyState(connection.readyState);
    },
    [setReadyState]
  );

  const onMessage = useCallback(
    ({ data }) => {
      const message = JSON.parse(data);
      setLastMessage(message);
    },
    [setLastMessage]
  );

  function sendMessage(message) {
    const msgJSON = JSON.stringify(message);
    connection.send(msgJSON);
  }

  useEffect(() => {
    connection.addEventListener('open', onReadyStateChange);
    connection.addEventListener('close', onReadyStateChange);
    connection.addEventListener('error', onReadyStateChange);
    connection.addEventListener('message', onMessage);

    return () => {
      connection.removeEventListener('open', onReadyStateChange);
      connection.removeEventListener('close', onReadyStateChange);
      connection.removeEventListener('error', onReadyStateChange);
      connection.removeEventListener('message', onMessage);
    };
  }, [connection]);

  return { lastMessage, readyState, sendMessage };
}

但是,我不知道如何正确地将钩子应用于连接。

  • 通过the rules of hooks,我不能在useEffect内部调用useConnection。它会向我抛出“无效的hook call”
  • 我也不能在useEffect之外引用connection.current,因为这违反了the rules of useRef:渲染期间不允许访问.current属性。

所以我的问题是,我如何将连接和挂钩结合在一起?或者我应该使用完全不同的方法?

osh3o9ms

osh3o9ms1#

如果你想这样做,例如。叫钩

const {lastMessage, readyState, sendMessage} = useConnection(something);

useEffect内部,因为它是唯一可以访问something的地方,所以这是不可能的。
钩子应该在函数组件的主体中被调用,而不是有条件的,也不是在一个循环中,你可以把它们看作是一个导入。

解决方案:

创建一个状态connection并将其值作为useConnection函数参数

const [connection, setConnection] = useState(null);
const { lastMessage, readyState, sendMessage } = useConnection(connection);

**注意:**您可能需要在钩子中做一些额外的工作来处理收到的null连接,因为这将在组件首次挂载时调用钩子时发生

现在,从useEffect开始,而不是调用钩子,你只需要用你想要调用你的自定义钩子的值(即something)来更新connection状态。
当状态被更新时,组件会重新呈现,如果钩子函数接收到一个新的值作为参数,它会被调用,因此当connection得到一个新的值时,它会返回一个新的{lastMessage, readyState, sendMessage}值,在这个新的呈现中,你可以访问它。
因此,如果您想在更新connection状态后直接在useEffect中使用它们,那么这是不可能,但是,您仍然可以创建另一个useEffect,每次更新connection时都会触发它,所以在组件重新-通过在第一个useEffect中更新connection来呈现,因为它是一个新的呈现,所以您可以访问第二个useEffect

useEffect(() => {
 // continue your logic here
 if(connection){
   sendMessage("new msg")
 }
},[connection?.id])

当执行connection.send(msgJSON)时,该函数应该与预期的connection对象一起工作,因为它是一个接收预期的connection的新示例。在这里,我添加了connection?.id作为依赖项,而不是connection,因为我们想避免包含对象,这样的键应该存在于connection对象中。

注意useEffect钩子在每次更新其依赖数组中的值时都会触发,而且在组件首次挂载时也会触发。

whhtz7ly

whhtz7ly2#

您可能希望在组件的顶层调用钩子,然后在useEffect()中使用它返回的部分

const {lastMessage, readyState, sendMessage} = useConnection(connection);

useEffect(() => {
    sendMessage("Hi");
}, [sendMessage]);

你的方法看起来太棒了!

bq8i3lrv

bq8i3lrv3#

最后,我重新设计了useConnection,使其采用工厂函数,而不是直接采用连接对象。这不仅绕过了useRef,还使调用代码更加清晰(并且与the React devs views更加一致)。下面是如何使用useConnection钩子:

const { lastMessage, readyState, sendMessage } = useConnection(
  () => new WebSocket(socketUrl)
);

这就是全部。(如果socketUrl是可变的,则可以将其 Package 在useMemo中。)useConnection现在看起来像这样:

/**
 * Provides a React-friendly interface around a JS connection.
 * It exposes two state variables: lastMessage and readyState,
 * and one method sendMessage(json).
 * factory is a factory function that creates a new connection.
 */
export function useConnection(factory) {
  const empty = () => {};

  let sendMessage = empty;
  const [lastMessage, setLastMessage] = useState('');
  const [readyState, setReadyState] = useState(null);

  // The two event handlers do not depend on the connection
  // and are therefore declared outside `useEffect`.

  const handleMessage = ({ data }) => {
    const message = JSON.parse(data);
    setLastMessage(message);
  };

  const handleReadyStateChange = (e) => {
    console.debug(e);
    setReadyState(e.target.readyState);
  };

  // The effect, setting up and tearing down the connection,
  // depends on the factory function.

  useEffect(() => {
    let conn = null;

    const connect = () => {
      console.log('✅ Connecting...');
      conn = factory();
      conn.addEventListener('open', handleReadyStateChange);
      conn.addEventListener('close', handleReadyStateChange);
      conn.addEventListener('error', handleReadyStateChange);
      conn.addEventListener('message', handleMessage);
      sendMessage = send;
    };

    const disconnect = () => {
      console.log('❌ Disconnecting...');
      conn.removeEventListener('open', handleReadyStateChange);
      conn.removeEventListener('close', handleReadyStateChange);
      conn.removeEventListener('error', handleReadyStateChange);
      conn.removeEventListener('message', handleMessage);
      // Avoid an error message in the console saying:
      // "WebSocket is closed before the connection is established."
      if (conn.readyState === 1) {
        conn.close();
      }
      sendMessage = empty;
      setReadyState(conn.readyState);
      conn = null;
    };

    // The send method does depend on the connection and so
    // is declared inside `useEffect`.

    const send = (message) => {
      if (conn) {
        const msgJSON = JSON.stringify(message);
        conn.send(msgJSON);
      }
    };

    connect();

    return disconnect;
  }, [factory]);

  return { lastMessage, readyState, sendMessage };
}

由于公认的答案与原来的问题较为接近,我将保持原样。

相关问题