Next.js Discord

Discord Forum

Custom hook vs React Context API for WebSocket connections

Unanswered
Spectacled bear posted this in #help-forum
Open in Discord
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 useContext(SignalRContext) to access the same connection
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.

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>