如何在
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
属性。所以我的问题是,如何将连接和挂钩结合在一起?或者我应该完全使用不同的方法?
如果你想这样做,即调用钩子
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
钩子都会触发,而且在组件首次挂载时也会触发。
您可能想要的是在组件的顶层调用您的钩子,然后在
useEffect()
中使用它返回的部分
const {lastMessage, readyState, sendMessage} = useConnection(connection);
useEffect(() => {
sendMessage("Hi");
}, [sendMessage]);
你的方法看起来棒极了!
最终,我重新设计了
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 };
}
接受的答案更接近原始问题,我将保持原样。