Custom hook vs React Context API for WebSocket connections
Unanswered
Spectacled bear posted this in #help-forum
Spectacled bearOP
I'm wondering about the diff between React Context API and custom hooks for managing websocket connections.
https://shaxadd.medium.com/how-to-use-signalr-in-a-react-app-step-by-step-645bd73aad2a#bypass
(SignalR implementation)
https://hackernoon.com/streaming-in-nextjs-15-websockets-vs-server-sent-events
(websocket and SSE implementation)
I’ve built a real-time dashboard where a SignalR connection needs to stay alive as long as the user is navigating through the dashboard pages.
I've achieved this already, I just want to understand more about the solution I copy/pasted.
afaik,
- custom hooks = each call creates its own independent WebSocket connection;
- React Context API = any child can
I think the reason is that SignalR hub is designed to be consumed once through a shared connection (it's even documented) whereas with raw websockets, you can create and manage as many independent connections as you want.
https://shaxadd.medium.com/how-to-use-signalr-in-a-react-app-step-by-step-645bd73aad2a#bypass
(SignalR implementation)
https://hackernoon.com/streaming-in-nextjs-15-websockets-vs-server-sent-events
(websocket and SSE implementation)
I’ve built a real-time dashboard where a SignalR connection needs to stay alive as long as the user is navigating through the dashboard pages.
I've achieved this already, I just want to understand more about the solution I copy/pasted.
afaik,
- custom hooks = each call creates its own independent WebSocket connection;
- React Context API = any child can
useContext(SignalRContext)
to access the same connectionI think the reason is that SignalR hub is designed to be consumed once through a shared connection (it's even documented) whereas with raw websockets, you can create and manage as many independent connections as you want.
import { useEffect, useRef, useState } from "react";
interface UseWebSocketOptions {
onOpen?: (event: Event) => void;
onMessage?: (event: MessageEvent) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
reconnectAttempts?: number;
reconnectInterval?: number;
}
export const useWebSocket = (
url: string,
options: UseWebSocketOptions = {}
) => {
const {
onOpen,
onMessage,
onClose,
onError,
reconnectAttempts = 5,
reconnectInterval = 3000,
} = options;
const [isConnected, setIsConnected] = useState(false);
const [isReconnecting, setIsReconnecting] = useState(false);
const webSocketRef = useRef<WebSocket | null>(null);
const attemptsRef = useRef(0);
const connectWebSocket = () => {
setIsReconnecting(false);
attemptsRef.current = 0;
const ws = new WebSocket(url);
webSocketRef.current = ws;
ws.onopen = (event) => {
setIsConnected(true);
setIsReconnecting(false);
if (onOpen) onOpen(event);
};
ws.onmessage = (event) => {
if (onMessage) onMessage(event);
};
ws.onclose = (event) => {
setIsConnected(false);
if (onClose) onClose(event);
// Attempt reconnection if allowed
if (attemptsRef.current < reconnectAttempts) {
setIsReconnecting(true);
attemptsRef.current++;
setTimeout(connectWebSocket, reconnectInterval);
}
};
ws.onerror = (event) => {
if (onError) onError(event);
};
};
useEffect(() => {
connectWebSocket();
// Cleanup on component unmount
return () => {
if (webSocketRef.current) {
webSocketRef.current.close();
}
};
}, [url]);
const sendMessage = (message: string) => {
if (
webSocketRef.current &&
webSocketRef.current.readyState === WebSocket.OPEN
) {
webSocketRef.current.send(message);
} else {
console.error("WebSocket is not open. Unable to send message.");
}
};
return { isConnected, isReconnecting, sendMessage };
};
7 Replies
Spectacled bearOP
the SignalR provider solution is included further down, but I had to upload it as a file since it wouldn’t let me paste it directly
tbh, I like how I'm consuming the websocket
and this is how I have to consume SignalR
I really like the websocket calls more, it looks more readable
if you're wondering why two different implementations, I just implemented the same thing once with SignalR once with websockets to compare the results, but it's really the same thing
Spectacled bearOP
I think I actually got it
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>useWebSocket (custom hook)</title>
</head>
<body>
<div id="root"></div>
<!-- React + ReactDOM -->
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
const { createRoot } = ReactDOM;
function useWebSocket(url) {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef(null);
useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
setIsConnected(true);
console.log("Connected:", url);
};
ws.onclose = () => {
setIsConnected(false);
console.log("Closed:", url);
};
return () => ws.close();
}, [url]);
const sendMessage = (msg) => {
wsRef.current?.send(msg);
};
return { isConnected, sendMessage };
}
function ComponentA() {
const { isConnected, sendMessage } = useWebSocket("wss://echo.websocket.events");
return (
<div>
<h3>Component A</h3>
<p>Connected? {isConnected ? "Yes" : "No"}</p>
<button onClick={() => sendMessage("Hello from A")}>Send A</button>
</div>
);
}
function ComponentB() {
const { isConnected, sendMessage } = useWebSocket("wss://echo.websocket.events");
return (
<div>
<h3>Component B</h3>
<p>Connected? {isConnected ? "Yes" : "No"}</p>
<button onClick={() => sendMessage("Hello from B")}>Send B</button>
</div>
);
}
function App() {
return (
<div>
<h1>Custom Hook: Two independent sockets</h1>
<ComponentA />
<ComponentB />
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocketProvider (shared)</title>
</head>
<body>
<div id="root"></div>
<!-- React + ReactDOM -->
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { createContext, useContext, useEffect, useRef, useState } = React;
const { createRoot } = ReactDOM;
const WebSocketContext = createContext(null);
function WebSocketProvider({ url, children }) {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef(null);
useEffect(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
setIsConnected(true);
console.log("Connected:", url);
};
ws.onclose = () => {
setIsConnected(false);
console.log("Closed:", url);
};
return () => ws.close();
}, [url]);
const sendMessage = (msg) => {
wsRef.current?.send(msg);
};
return (
<WebSocketContext.Provider value={{ isConnected, sendMessage }}>
{children}
</WebSocketContext.Provider>
);
}
function useWebSocket() {
return useContext(WebSocketContext);
}
function ComponentA() {
const { isConnected, sendMessage } = useWebSocket();
return (
<div>
<h3>Component A</h3>
<p>Connected? {isConnected ? "Yes" : "No"}</p>
<button onClick={() => sendMessage("Hello from A")}>Send A</button>
</div>
);
}
function ComponentB() {
const { isConnected, sendMessage } = useWebSocket();
return (
<div>
<h3>Component B</h3>
<p>Connected? {isConnected ? "Yes" : "No"}</p>
<button onClick={() => sendMessage("Hello from B")}>Send B</button>
</div>
);
}
function App() {
return (
<WebSocketProvider url="wss://echo.websocket.events">
<h1>Context Provider: One shared socket</h1>
<ComponentA />
<ComponentB />
</WebSocketProvider>
);
}
createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>