How to Set Up SignalR with a React Frontend and C# Backend
To set up SignalR with React and a C# backend, you need three things: a SignalR hub on your .NET backend, the @microsoft/signalr npm package on your React frontend, and a CORS policy that allows the React dev server's origin. The hub defines server-side methods that the client can invoke, and the client registers handlers for messages that the server pushes. The connection uses WebSockets by default, with automatic fallback to Server-Sent Events or Long Polling.
Create the SignalR Hub in C#
A hub is a class that inherits from Hub. Each public method becomes an endpoint that the client can call. The following example shows a minimal chat hub that broadcasts messages to all connected clients.
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
// Broadcast a message to every connected client.
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public override async Task OnConnectedAsync()
{
// Log or track the new connection.
Console.WriteLine($"Client connected: {Context.ConnectionId}");
await base.OnConnectedAsync();
}
}
Configure the .NET Backend for SignalR
Register SignalR services, map the hub to a route, and configure CORS. The CORS policy is the most common source of "connection refused" errors. You must allow the React dev server origin and call AllowCredentials() because SignalR's WebSocket transport sends cookies.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
builder.Services.AddCors(options =>
{
options.AddPolicy("ReactApp", policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
// Required for SignalR's WebSocket/SSE transports.
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors("ReactApp");
app.MapHub("/chathub");
app.Run();
Install the SignalR Client Package
Install Microsoft's official SignalR client for JavaScript. Don't use the deprecated @aspnet/signalr package. It targets the older SignalR Core preview and lacks reconnection support and MessagePack negotiation.
npm install @microsoft/signalr
Build a Reusable React Hook for SignalR
Wrap the connection lifecycle in a custom hook so that every component needing real-time data can share a single connection. The key gotcha is that HubConnectionBuilder creates a connection object that you must start exactly once. You must also clean it up when the component unmounts to avoid memory leaks and duplicate event handlers.
import { useEffect, useRef, useState } from "react";
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
export function useSignalR(hubUrl) {
const [connection, setConnection] = useState(null);
const connectionRef = useRef(null);
useEffect(() => {
const conn = new HubConnectionBuilder()
.withUrl(hubUrl)
// Automatically reconnect with exponential backoff.
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(LogLevel.Information)
.build();
connectionRef.current = conn;
conn.start().then(() => setConnection(conn));
// Stop the connection when the component unmounts.
return () => { conn.stop(); };
}, [hubUrl]);
return connection;
}
Use the SignalR Hook in a React Component
The component registers a handler for ReceiveMessage (the same event name that the hub sends via SendAsync) and invokes the hub's SendMessage method. Note that you must call connection.on before or immediately after the connection starts. If you register it too late, you'll miss messages that arrive during the gap.
import { useState, useEffect } from "react";
import { useSignalR } from "./useSignalR";
export default function Chat() {
const connection = useSignalR("http://localhost:5000/chathub");
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
useEffect(() => {
if (!connection) return;
// Register the handler for incoming messages.
connection.on("ReceiveMessage", (user, text) => {
setMessages(prev => [...prev, { user, text }]);
});
return () => connection.off("ReceiveMessage");
}, [connection]);
const send = async () => {
if (connection) await connection.invoke("SendMessage", "React User", input);
setInput("");
};
return (
{messages.map((m, i) => (
- {m.user}: {m.text}
))}
setInput(e.target.value)} />
);
}
Common Gotchas and Production Concerns
CORS with credentials: When you call AllowCredentials(), you can't use AllowAnyOrigin(). You must specify exact origins. This trips up almost everyone during initial setup because the browser shows a generic CORS failure with no mention of credentials.
Reconnection state: withAutomaticReconnect doesn't re-register your .on handlers; those survive reconnection. However, if the server restarted, your client's connection ID changes, and the server loses any group memberships. Handle this in the hub's OnConnectedAsync by re-adding the user to their groups, or listen to the onreconnected event on the client to re-join.
Stale closures in React: If you access React state inside a connection.on callback, you capture a stale closure. Use a ref or the functional form of setState (as shown above with prev => [...prev, msg]) to always work with current state.
JWT authentication: SignalR's WebSocket transport can't send custom HTTP headers after the initial handshake. Pass the token via the accessTokenFactory option instead. SignalR sends it as a query string parameter for WebSockets and as a Bearer header for other transports.
const conn = new HubConnectionBuilder()
.withUrl("http://localhost:5000/chathub", {
// Provide a factory function that returns the current token.
accessTokenFactory: () => localStorage.getItem("jwt_token")
})
.withAutomaticReconnect()
.build();
SignalR vs. Raw WebSockets: When to Choose Which
Use SignalR unless you have a specific reason not to. It gives you automatic transport negotiation (WebSocket → SSE → Long Polling), built-in reconnection, hub-based routing, group management, and strongly-typed clients. You'd have to build all of that yourself on raw WebSockets. The only reason to drop down to raw WebSockets is if you're building something protocol-specific (like a game server with a binary protocol) or you need to avoid the negotiation round-trip for sub-millisecond connection setup. For typical business applications with a React frontend, SignalR eliminates an entire class of reliability bugs that you'd otherwise have to solve manually.