I’m working on a React application where I need to manage multiple WebSocket connections. Specifically, I want different components to be able to share a connection if they use the same URL. I’m using SignalR for WebSocket communication and have implemented a context and a custom hook for managing these connections.
The goal is to have a single WebSocket connection per URL that can be shared among multiple components, and properly manage the lifecycle of these connections (i.e., establish, share, and disconnect connections as needed).
I have tried a bunch of different things to get the desired behaviour, but sadly im unable to create a satisfactory solution. Currently, I’m experiencing an issue where the WebSocket connection is established correctly but then unmounts and removes itself almost immediately. In general i have had a lot of problems regarding “race condtions”, which have been caused by multiple components simuntaniously trying to establish a connection. Any help would be much appreaciated.
Here is the relevant code. If you feel any code is missing or that my question is unclear, i would love to further explain the problem.
import { HubUrls, SignalRContext } from "@/providers/SignalRProvider";
import { HubConnection } from "@microsoft/signalr";
import { useContext, useEffect, useState } from "react";
const useSignalR = (url: HubUrls): { connection: HubConnection | null, error: Error | null, loading: boolean } => {
const context = useContext(SignalRContext);
const [connection, setConnection] = useState<HubConnection | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
if (!context) {
throw new Error('useSignalR must be used within a SignalRProvider');
}
useEffect(() => {
let isMounted = true;
const setupConnection = async () => {
try {
const conn = await context.getConnection(url);
if (isMounted) {
setConnection(conn);
setLoading(false);
}
} catch (err) {
if (isMounted) {
setError(err as Error);
setLoading(false);
}
}
};
setupConnection();
return () => {
isMounted = false;
context.releaseConnection(url);
};
}, [context, url]);
return { connection, error, loading };
};
export default useSignalR;
"use client";
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import useAuthContext from "@/providers/AuthProvider";
export const BASE_URL = 'http://localhost:5040';
export type HubUrls = '/chat' | '/kanban' | '/logbook';
const createConnection = (url: HubUrls, getToken: () => Promise<string>): HubConnection => {
return new HubConnectionBuilder()
.withUrl(`${BASE_URL}${url}`, {
accessTokenFactory: getToken,
})
.configureLogging(LogLevel.Information)
.withAutomaticReconnect()
.build();
};
const globalHubRegistry: Record<string, { connection: Promise<HubConnection>, subscribers: number }> = {};
const getOrCreateWebSocket = async (url: HubUrls, getToken: () => Promise<string>): Promise<HubConnection> => {
if (globalHubRegistry[url]) {
globalHubRegistry[url].subscribers += 1;
return globalHubRegistry[url].connection;
} else {
const connection = createConnection(url, getToken);
const connectionPromise = connection.start().then(() => {
globalHubRegistry[url] = { connection: Promise.resolve(connection), subscribers: 1 };
return connection;
}).catch(err => {
delete globalHubRegistry[url];
throw err;
});
globalHubRegistry[url] = { connection: connectionPromise, subscribers: 1 };
return connectionPromise;
}
};
const unsubscribeWebSocket = async (url: HubUrls): Promise<void> => {
if (globalHubRegistry[url]) {
globalHubRegistry[url].subscribers -= 1;
if (globalHubRegistry[url].subscribers === 0) {
console.log("Deleting connection" + url);
const connection = await globalHubRegistry[url].connection;
await connection.stop();
delete globalHubRegistry[url];
}
}
};
interface SignalRContextProps {
getConnection: (url: HubUrls) => Promise<HubConnection>;
releaseConnection: (url: HubUrls) => void;
}
export const SignalRContext = createContext<SignalRContextProps | undefined>(undefined);
const SignalRProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { getToken } = useAuthContext();
const getConnection = (url: HubUrls): Promise<HubConnection> => {
return getOrCreateWebSocket(url, getToken);
};
const releaseConnection = async (url: HubUrls): Promise<void> => {
await unsubscribeWebSocket(url);
};
return (
<SignalRContext.Provider value={{ getConnection, releaseConnection }}>
{children}
</SignalRContext.Provider>
);
};
export default SignalRProvider;
MertzAndeas is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.