I'm using websockets in my React application. The application is supposed to consist of function components only.
Imagine the application consists of two "tabs": one irrelevant, and the other one, a chat using websockets. Given that, we want to whip up a websocket connection once the user enters the chat tab.
How should the component handle the websocket object? Since we want to clean up once the user switches back to the other tab (WebSocket.close()
), it sounds like we should be using an effect hook here.
const Chat = () => {
const [messages, setMessages] = useState([]);
useEffect(() => {
const webSocket = new WebSocket("ws://url");
webSocket.onmessage = (message) => {
setMessages(prev => [...prev, message.data]);
};
return () => webSocket.close();
}, []);
return <p>{messages.join(" ")}</p>;
};
Works! But now, imagine we want to reference the webSocket
variable somewhere outside the useEffect
scope - say, we want to send something to the server once the user clicks a button.
Now, how should it be implemented? My current idea (albeit a flawed one, I feel) is:
const Chat = () => {
const [messages, setMessages] = useState([]);
const [webSocket] = useState(new WebSocket("ws://url"));
useEffect(() => {
webSocket.onmessage = (message) => {
setMessages(prev => [...prev, message.data]);
};
return () => webSocket.close();
}, []);
return <p>{messages.join(" ")}</p>;
};
Kinda works, nevertheless I feel like there's a better solution.
Avoid using WebSockets if only a small number of messages will be sent or if the messaging is very infrequent. Unless the client must quickly receive or act upon updates, maintaining the open connection may be an unnecessary waste of resources.
There are multiple ways of adding WebSocket support to a React app. Each method has its pros and cons. This guide will go through some of the common patterns but will only explore in detail the pattern we are implementing.
WebSocket uses a unified TCP connection and needs one party to terminate the connection. Until it happens, the connection remains active. HTTP needs to build a distinct connection for separate requests. Once the request is completed, the connection breaks automatically.
Websockets are largely obsolete because nowadays, if you create a HTTP/2 fetch request, any existing keepalive connection to that server is used, so the overhead that pre-HTTP/2 XHR connections needed is lost and with it the advantage of Websockets.
As @skyboyer has written in a comment under your question, you could use useRef
hook to hold WebSocket
and it will be more correct since you are not going to change or recreate WebSocket object. So you don't need useState
hook.
The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. more
So you could change your code to:
const Chat = () => {
const [messages, setMessages] = useState([]);
const webSocket = useRef(null);
useEffect(() => {
webSocket.current = new WebSocket("ws://url");
webSocket.current.onmessage = (message) => {
setMessages(prev => [...prev, message.data]);
};
return () => webSocket.current.close();
}, []);
return <p>{messages.join(" ")}</p>;
};
probably a good option is to have in a separate WS instance for manage the WS operations and used it where you need it, for example.
class WebSocketClient {
static instance = null;
callbacks = {};
static getInstance() {
if (!WebSocketClient.instance) WebSocketClient.instance = new WebSocketClient();
return WebSocketClient.instance;
}
constructor() {
this.socketRef = null;
}
addCallbacks = (...callbacks) => this.callbacks = { ...callbacks };
connect = () => {
const path = 'YOUR_SOCKET_PATH';
this.socketRef = new WebSocket(path);
this.socketRef.onopen = () => {
console.log('WebSocket open');
};
this.socketRef.onmessage = e => {
this.socketNewMessage(e.data);
};
this.socketRef.onerror = e => {
console.log(e.message);
};
this.socketRef.onclose = () => {
console.log("WebSocket closed let's reopen");
this.connect();
};
}
state = () => this.socketRef.readyState;
waitForSocketConnection = (callback) => {
const socket = this.socketRef;
const recursion = this.waitForSocketConnection;
setTimeout(
() => {
if (socket.readyState === 1) {
console.log("Connection is made")
if (callback != null) {
callback();
}
return;
} else {
console.log("wait for connection...")
recursion(callback);
}
},
1);
}
}
export default WebSocketClient.getInstance();
So in your component will be.
import React, { useEffect } from 'react';
import WSC from 'wsc';
const Test = ({}) => {
useEffect(()=>{
WSC.connect();
WSC.waitForSocketConnection(()=>{
'HERE_YOUR_CALLBACKS'
});
},[])
}
With this way always return the same instance.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With