use-signalr

Quick start

Install the package, declare a contract, and call a hub from a component.

Requirements

  • Node 20.19 or later.
  • @microsoft/signalr 8, 9, or 10.
  • A server with ASP.NET Core SignalR. The older ASP.NET SignalR for .NET Framework uses a different protocol and does not work with this client.
  • React 19.

Install

Install the package and its SignalR peer:

npm install @dammers/use-signalr-react @microsoft/signalr

Set up

Write the server hub

The samples in this documentation use this hub:

RoomHub.cs
using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR;

namespace Server.Hubs;

public record ChatMessage(string RoomId, string User, string Text, DateTimeOffset SentAt);

/// <summary>Teaching hub: chat rooms built on SignalR groups.</summary>
public class RoomHub : Hub
{
    private const int HistoryLimit = 50;
    private static readonly ConcurrentDictionary<string, Queue<ChatMessage>> History = new();

    // Adding a connection that is already in the group changes nothing, so the client can retry.
    public Task JoinRoom(string roomId) => Groups.AddToGroupAsync(Context.ConnectionId, roomId);

    public Task LeaveRoom(string roomId) => Groups.RemoveFromGroupAsync(Context.ConnectionId, roomId);

    public Task SendMessage(string roomId, string text)
    {
        var message = new ChatMessage(roomId, Context.User?.Identity?.Name ?? "anonymous", text, DateTimeOffset.UtcNow);
        var history = History.GetOrAdd(roomId, _ => new Queue<ChatMessage>());
        lock (history)
        {
            history.Enqueue(message);
            if (history.Count > HistoryLimit) history.Dequeue();
        }
        return Clients.Group(roomId).SendAsync("MessageReceived", message);
    }

    public ChatMessage[] GetHistory(string roomId)
    {
        if (!History.TryGetValue(roomId, out var history)) return [];
        lock (history) return history.ToArray();
    }
}

Declare the contract

List each hub path, its events, and its methods. Names and argument types must match the server hub:

contract.ts
import { event, method } from "@dammers/use-signalr-react";

export interface ChatMessage {
  roomId: string;
  user: string;
  text: string;
  sentAt: string;
}

// Keys are hub paths. Names and argument types match the server hub.
export const hubs = {
  "/hubs/rooms": {
    events: {
      MessageReceived: event<[message: ChatMessage]>(),
    },
    methods: {
      JoinRoom: method<[roomId: string]>(),
      LeaveRoom: method<[roomId: string]>(),
      SendMessage: method<[roomId: string, text: string]>(),
      GetHistory: method<[roomId: string], ChatMessage[]>(),
    },
  },
  "/hubs/presence": {
    lazy: true,
    graceMs: 2000,
    events: {
      OnlineCount: event<[count: number]>(),
    },
  },
};

Create the client

Pass the contract to createSignalRClient. The call returns the typed helpers that your components use:

client.ts
"use client";

import { createSignalRClient } from "@dammers/use-signalr-react";
import { hubs } from "./contract";

export const {
  SignalRProvider,
  useSignalREffect,
  useSignalRInvoke,
  useSignalRSend,
  useSignalRTeardown,
  useHubStatus,
  useOnReconnected,
} = createSignalRClient({ hubs });

Start the session

Give SignalRProvider the base URL of your server and a function that returns the access token:

provider.tsx
"use client";

import type { ReactNode } from "react";
import { SignalRProvider } from "./client";

export function Providers({ children }: { children: ReactNode }) {
  const { isSignedIn, getToken } = useAuth();

  return (
    <SignalRProvider
      baseUrl="https://api.example.com"
      accessTokenFactory={getToken}
      enabled={isSignedIn}
    >
      {children}
    </SignalRProvider>
  );
}

Use a hub in a component

This component shows the messages of a room and sends one message:

quick-start.tsx
import { useEffect, useState } from "react";
import type { ChatMessage } from "./contract";
import { useHubStatus, useSignalREffect, useSignalRInvoke } from "./client";
import { ignoreAbort } from "./ignore-abort";

export function Room({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const status = useHubStatus("/hubs/rooms");
  const joinRoom = useSignalRInvoke("/hubs/rooms", "JoinRoom");
  const sendMessage = useSignalRInvoke("/hubs/rooms", "SendMessage");

  useEffect(() => {
    if (status === "connected") joinRoom(roomId).catch(ignoreAbort);
  }, [status, roomId, joinRoom]);

  useSignalREffect("/hubs/rooms", "MessageReceived", (message) => {
    if (message.roomId === roomId) setMessages((all) => [...all, message]);
  });

  return (
    <div>
      <ul>
        {messages.map((m, index) => (
          <li key={index}>{m.user}: {m.text}</li>
        ))}
      </ul>
      <button
        disabled={status !== "connected"}
        onClick={() => sendMessage(roomId, "Hello").catch(ignoreAbort)}
      >
        Say hello
      </button>
    </div>
  );
}

ignoreAbort ignores the AbortError that a call gets when the component unmounts while the call waits. See Calls.

Run the example app

The repository has a runnable example app with a matching server.

Open examples/react.

Next, read Client config and Connection status.

On this page