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.
  • Angular 20, 21, or 22. The rxjs-interop entry point also needs rxjs 7.6 or later.

Install

Install the package and its SignalR peer:

npm install @dammers/use-signalr-angular @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-angular";

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
import { createSignalRClient } from "@dammers/use-signalr-angular";
import { hubs } from "./contract";

export const {
  provideSignalR,
  injectHubEvent,
  injectHubInvoke,
  injectHubSend,
  injectHubTeardown,
  injectHubStatus,
  injectOnReconnected,
  injectKeepHubAlive,
} = createSignalRClient({ hubs });

Start the session

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

provider.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideSignalR(() => {
      const auth = inject(AuthService);
      return {
        baseUrl: "https://api.example.com",
        accessTokenFactory: () => auth.token(),
        enabled: auth.isLoggedIn,
        onError: (hub, error, info) => console.error(hub, info.source, error),
      };
    }),
  ],
};

Use a hub in a component

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

quick-start.ts
import { Component, effect, signal } from "@angular/core";
import { injectHubEvent, injectHubInvoke, injectHubStatus } from "./client";
import { ignoreAbort } from "./ignore-abort";
import type { ChatMessage } from "./contract";

@Component({
  selector: "app-room",
  template: `
    <ul>
      @for (message of messages(); track $index) {
        <li>{{ message.user }}: {{ message.text }}</li>
      }
    </ul>
    <button [disabled]="status() !== 'connected'" (click)="send()">Say hello</button>
  `,
})
export class RoomComponent {
  roomId = "general";
  messages = signal<ChatMessage[]>([]);
  status = injectHubStatus("/hubs/rooms");
  private joinRoom = injectHubInvoke("/hubs/rooms", "JoinRoom");
  private sendMessage = injectHubInvoke("/hubs/rooms", "SendMessage");

  constructor() {
    effect(() => {
      if (this.status() === "connected") this.joinRoom(this.roomId).catch(ignoreAbort);
    });
    injectHubEvent("/hubs/rooms", "MessageReceived", (message) => {
      if (message.roomId === this.roomId) this.messages.update((list) => [...list, message]);
    });
  }

  send() {
    this.sendMessage(this.roomId, "Hello").catch(ignoreAbort);
  }
}

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/angular.

Next, read Client config and Connection status.

On this page