我不相信效果是最好的地方。如果它是应用程序级的,在它自己的模块中实现它可能会更简单,并在需要时将其引入。
不过,要使其正常工作,您应该考虑管理两个独立的生命周期:组件生命周期和 websocket 生命周期。要使其按您的意愿工作,您必须确保一个中的每个状态更改都与另一个中的状态更改保持一致。
首先,请记住,每次数组中的依赖项发生更改时,您的效果都会运行。因此,在您的示例中,您的效果会在您每次设置 wsState 时运行。
要记住的另一件事是,每次wsState 更改时都会调用您的清理函数,这会在您的效果中执行两次(打开时将其设置为true,关闭时设置为false) .这意味着,当您创建一个新套接字时,它无法连接,关闭事件会触发,并将状态更改排队。
每次尝试连接时,它会将wsState 设置为true(排队重新运行您的效果),尝试连接但失败,最后设置另一个超时,将状态更新为false .但是,在效果再次运行之前,尝试将状态设置为 true 等。
要解决此问题,请从效果生命周期开始。你的效果应该在什么时候运行?什么时候应该清理?一些想法:
- 效果应在第一次渲染期间运行一次,但在后续渲染期间不应运行
- WebSocket断开连接时应该清理效果
- 效果应该在超时后重新运行,触发重新连接
这对组件意味着什么?您不想将 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>
);
}
在此示例中,连接状态被跟踪,但不在 useEffect 依赖项中。不过,waitingForReconnect 是。它在连接关闭时设置,并在一段时间后取消设置,以触发重新连接尝试。
清理也会触发关闭,因此我们需要区分 onClose,我们通过查看客户端是否已被取消引用来做到这一点。
如您所见,这种方法相当复杂,并且它将 WS 生命周期与组件生命周期联系起来(如果您在应用程序级别执行此操作,这在技术上是可以的)。
但是,一个主要的警告是stale closures 很容易遇到问题。例如,addMessage 可以访问局部变量 messages,但由于 addMessage 没有作为依赖项传入,因此您不能在每次运行效果时调用它两次,否则它将覆盖最后一条消息。 (它本身不是覆盖;它实际上只是用旧的“陈旧”值messages 覆盖状态,并与新值连接。调用它十次,你只会看到最后一个值。)
因此,您可以将 addMessage 添加到依赖项中,但是您将在每次渲染时断开并重新连接 websocket。你可以去掉addMessages,然后把那个逻辑移到效果中,但是每次你更新messages数组时它都会重新运行(比每次渲染的频率低,但仍然太频繁)。
因此,我建议您在应用生命周期之外设置您的客户端。您可以使用自定义挂钩来处理传入的消息,或者直接在效果中处理它们。
这是一个例子:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
function reconnectingSocket(url) {
let client;
let isConnected = false;
let reconnectOnClose = true;
let messageListeners = [];
let stateChangeListeners = [];
function on(fn) {
messageListeners.push(fn);
}
function off(fn) {
messageListeners = messageListeners.filter(l => l !== fn);
}
function onStateChange(fn) {
stateChangeListeners.push(fn);
return () => {
stateChangeListeners = stateChangeListeners.filter(l => l !== fn);
};
}
function start() {
client = new WebSocket(URL);
client.onopen = () => {
isConnected = true;
stateChangeListeners.forEach(fn => fn(true));
}
const close = client.close;
// Close without reconnecting;
client.close = () => {
reconnectOnClose = false;
close.call(client);
}
client.onmessage = (event) => {
messageListeners.forEach(fn => fn(event.data));
}
client.onerror = (e) => console.error(e);
client.onclose = () => {
isConnected = false;
stateChangeListeners.forEach(fn => fn(false));
if (!reconnectOnClose) {
console.log('ws closed by app');
return;
}
console.log('ws closed by server');
setTimeout(start, 3000);
}
}
start();
return {
on,
off,
onStateChange,
close: () => client.close(),
getClient: () => client,
isConnected: () => isConnected,
};
}
const client = reconnectingSocket(URL);
function useMessages() {
const [messages, setMessages] = useState([]);
useEffect(() => {
function handleMessage(message) {
setMessages([...messages, message]);
}
client.on(handleMessage);
return () => client.off(handleMessage);
}, [messages, setMessages]);
return messages;
}
export default function App() {
const [message, setMessage] = useState('');
const messages = useMessages();
const [isConnected, setIsConnected] = useState(client.isConnected());
useEffect(() => {
return client.onStateChange(setIsConnected);
}, [setIsConnected]);
useEffect(() => {
if (isConnected) {
client.getClient().send('hi');
}
}, [isConnected]);
function sendMessage(e) {
e.preventDefault();
client.getClient().send(message);
setMessage('');
}
return (
<div>
<h1>Websocket {isConnected ? 'Connected' : 'Disconnected'}</h1>
<form onSubmit={sendMessage}>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">Send</button>
</form>
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}