I'm attempting to use RTK Query with a signalR websocket service to keep my react app up to date with server data.
My RTK API looks like this:
export const hubConnection = new signalR.HubConnectionBuilder() .withUrl("https://localhost:7152/messagehub", { withCredentials: false, skipNegotiation: true, logMessageContent: true, logger: signalR.LogLevel.Information, transport: signalR.HttpTransportType.WebSockets }) .withAutomaticReconnect([0, 1000, 5000, 10000, 30000]) .build();export const apiSlice = createApi({ reducerPath: 'apiSlice', baseQuery: fetchBaseQuery({ baseUrl: 'https://localhost:7152' }), endpoints: (builder) => ({ // these are the websocket / signalR endpoints getNotifications: builder.query<NotificationType[], void>({ query: () => 'messagehub', async onCacheEntryAdded( arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }, ) { console.log(hubConnection.state); // PRINTS Disconnected try { // this loads the existing cache data // await cacheDataLoaded; IF I UNCOMMENT THIS LINE, NOTHING FURTHER HAPPENS if (hubConnection.state === signalR.HubConnectionState.Disconnected) { console.log('SignalR connection is disconnected. Starting connection...'); // THIS MESSAGE PRINTS TO CONSOLE await hubConnection.start().then(() => console.log("SignalR connection established")); // THIS MESSAGE PRINTS TO CONSOLE } hubConnection.on("ReceiveMessage", (notification: NotificationType) => { console.log('StatusMessage', notification); // WITH THE cacheDataLoaded CALL COMMENTED OUT, THIS PRINTS WHEN A MESSAGE IS RECEIVED updateCachedData((draft) => { console.log('cache updating', draft); // THIS MESSAGE NEVER PRINTS draft.push(notification) // THE STORE STAYS EMPTY }) }); } catch (error) { console.error('Failed to connect to SignalR hub:', JSON.parse(JSON.stringify(error))); } // this is a promise that resolves when the socket is no longer being used await cacheEntryRemoved; // close signalR connection await hubConnection.stop(); } }), }),})First, theawait cacheDataLoaded method, if I leave it uncommented, blocks anything else from running. When I comment that out, I see an error on reload that says "Connection ID required", and while I know this is coming from SignalR, I don't know where / why.
In Redux devtools, the status ofapiSlice.queries.getNotifications showsstatus: rejected. Again, I don't understand this message or where it's coming from-- there's nothing in logs / console showing any error in the code.
When I fire a test message from the signalR side, I see the message logged on the RTK Query side in theReceiveMessage handler, but theupdateCachedData method never fires. I wouldn't expect theReceiveMessage handler to get fired at all if thegetNotifications API was in arejected status?
Anyone got signalR websockets running with RTK Query? I've seen plenty of examples using signalR as middleware, and I've been able to get signalR running in a single component-- but I'm hoping to leverage the RTK Query side of things and so far I stumped.
- What is cacheDataLoaded method and where are you calling it?SoftwareDveloper– SoftwareDveloper2025-03-26 18:48:15 +00:00CommentedMar 26 at 18:48
cachedDataLoadedis a method ononCacheEntryAdded, It's commented out in the code above.user101289– user1012892025-03-26 19:30:34 +00:00CommentedMar 26 at 19:30- You probably want to experiment with slice/thunk and middleware first. The query would expect to receive some result, otherwise it’s likely rejected on timeout.antokhio– antokhio2025-03-26 20:44:45 +00:00CommentedMar 26 at 20:44
- I've used slices / thunks in the past-- from what I was reading in the docs this seems to be the newer way to do this to eliminate all that. . .user101289– user1012892025-03-26 23:15:55 +00:00CommentedMar 26 at 23:15
- I am trying to modified the code snippet,please kindly check it and let me know if it works for you.buymeacoffee– buymeacoffee2025-03-27 01:57:44 +00:00CommentedMar 27 at 1:57
1 Answer1
Using below code can fix the issue. Here are my improvements and comparison with the previous code.
| Issue | Original Code Flaw | Revised Code Improvement |
|---|---|---|
| Invalid HTTP Request | Usedquery: () => 'messagehub' to send HTTP GET to SignalR Hub endpoint. | Replaced withqueryFn: () => ({ data: [] }) to skip HTTP requests and avoid rejection. |
| SignalR Instance Management | Exported a directhubConnection instance, risking multiple connections. | Implemented singleton pattern viagetHubConnection() to ensure a single global instance. |
| cacheDataLoaded Blocking | Unhandledawait cacheDataLoaded exception disrupted SignalR connection logic. | Addedtry/catch to safely handlecacheDataLoaded errors, allowing SignalR to proceed. |
| Connection Lifecycle | No cleanup for event listeners or connection termination on component unmount. | AddedhubConnection.off("ReceiveMessage") andhubConnection.stop() oncacheEntryRemoved. |
| Cache Update Reliability | Mutateddraft directly (draft.push(...)), risking Immer update detection issues. | Used immutable update (return [...draft, notification]) to ensure Immer tracks changes. |
| Error Handling | Partialtry/catch coverage left critical operations unhandled. | Unified error handling around SignalR connection, message listeners, and teardown logic. |
// signalr.tsimport * as signalR from '@microsoft/signalr';let hubConnection: signalR.HubConnection;export const getHubConnection = () => { if (!hubConnection) { hubConnection = new signalR.HubConnectionBuilder() .withUrl("https://localhost:7152/messagehub", { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }) .withAutomaticReconnect([0, 1000, 5000, 10000, 30000]) .build(); } return hubConnection;};// apiSlice.tsimport { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';import { getHubConnection } from './signalr';export const apiSlice = createApi({ reducerPath: 'apiSlice', baseQuery: fetchBaseQuery({ baseUrl: 'https://localhost:7152' }), endpoints: (builder) => ({ getNotifications: builder.query<NotificationType[], void>({ queryFn: () => ({ data: [] }), async onCacheEntryAdded( arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }, ) { const hubConnection = getHubConnection(); try { await cacheDataLoaded; } catch (error) { console.log(error); } try { if (hubConnection.state === signalR.HubConnectionState.Disconnected) { await hubConnection.start(); } hubConnection.on("ReceiveMessage", (notification: NotificationType) => { updateCachedData((draft) => { return [...draft, notification]; }); }); await cacheEntryRemoved; hubConnection.off("ReceiveMessage"); await hubConnection.stop(); } catch (error) { console.error('SignalR error message:', error); } } }), }),});export const { useGetNotificationsQuery } = apiSlice;1 Comment
draft.push should be perfectly acceptable, and is recommended over older verbose immutable update patterns. I think you could remove the "Cache Update Reliability" table row. Great breakdown otherwise!Explore related questions
See similar questions with these tags.

