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

问题描述 投票:0回答:3

如何在

useEffect
内部设置连接,同时使用自定义钩子装饰该连接?自定义钩子不允许在
useEffect
内部运行,并且在渲染期间不允许
ref.current
运行。使用 React 友好的界面包装连接的正确方法是什么?

我相信在 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('...')
open
close
error
message
事件相同的连接接口。

我正在寻找一种方法来为这些连接提供类似 React 的接口。我在想类似下面的自定义

useConnection
钩子,它接受一个 JavaScript 连接对象,并作为回报提供两个 React 状态变量
lastMessage
readyState
,以及一个方法
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 };
}

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

  • 根据钩子规则,我无法在
    useConnection
    内调用
    useEffect
    。它向我抛出“无效的挂钩调用”。
  • 我也不能在
    connection.current
    之外引用
    useEffect
    ,因为这违反了
    useRef
    的规则:渲染期间不允许访问
    .current
    属性。

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

reactjs react-hooks websocket webrtc
3个回答
0
投票

如果你想这样做,即调用钩子

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

从内部

useEffect
因为这是您唯一可以访问
something
的地方,所以这是不可能的。
钩子应该在函数组件的主体中无条件地调用,并且不要在循环内调用,您可以将它们视为导入。

解决方案:

创建一个状态

connection
并将其值用作
useConnection
函数参数

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

注意:您可能需要在钩子中做一些额外的工作来处理收到的

null
连接,因为当在组件首次挂载中调用钩子时会发生这种情况

现在,从

useEffect
而不是调用钩子,您只需使用要调用自定义钩子的值更新
connection
状态,即
something

当状态更新时,组件将重新渲染,并且如果它接收到新值作为参数,您的钩子函数将被调用,因此当
connection
获取新值时,它会返回新的
{lastMessage, readyState, sendMessage} 
在这个新渲染中您可以访问的值

因此,如果您想在更新

useEffect
状态后直接在
connection
中使用它们,那么这是不可能的,但是,您仍然可以创建另一个
useEffect
,每次更新
connection
时都会触发,因此在组件通过更新第一个
connection
中的
useEffect
重新渲染后,因为它是一个新渲染,您将可以访问第二个钩子返回的新值
useEffect

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

该函数在执行

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

注意,每次更新其依赖项数组中的值时,

useEffect
钩子都会触发,而且在组件首次挂载时也会触发。


0
投票

您可能想要的是在组件的顶层调用您的钩子,然后在

useEffect()

中使用它返回的部分
const {lastMessage, readyState, sendMessage} = useConnection(connection);

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

你的方法看起来棒极了!


0
投票

最终,我重新设计了

useConnection
以采用工厂函数而不是直接连接对象。这不仅绕过了
useRef
,还使调用代码更加清晰(并且与React开发者视图更加一致)。现在这是如何使用
useConnection
钩子:

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

仅此而已。 (如果

useMemo
是可变的,则可以将其包装在
socketUrl
中。)
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 };
}

接受的答案更接近原始问题,我将保持原样。

© www.soinside.com 2019 - 2024. All rights reserved.