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.
  • Svelte 5.15 or later. Svelte 4 is not supported.

Install

Install the package and its SignalR peer:

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

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-svelte";
import { hubs } from "./contract";

export const {
  provideSignalR,
  getSignalR,
  keepHubAlive,
  onHubEvent,
  hubInvoke,
  hubSend,
  hubTeardown,
  hubStatus,
  onReconnected,
} = createSignalRClient({ hubs });

Start the session

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

provider.svelte
<script lang="ts">
  import type { Snippet } from "svelte";
  import { toStore } from "svelte/store";
  import { provideSignalR } from "./client";

  let { token, children }: { token: string | undefined; children: Snippet } = $props();

  provideSignalR({
    baseUrl: "https://api.example.com",
    accessTokenFactory: () => token ?? "",
    enabled: toStore(() => token !== undefined),
  });
</script>

{@render children()}

Use a hub in a component

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

quick-start.svelte
<script lang="ts">
  import { hubInvoke, hubStatus, onHubEvent } from "./client";
  import { ignoreAbort } from "./ignore-abort";
  import type { ChatMessage } from "./contract";

  let { roomId }: { roomId: string } = $props();
  let messages = $state<ChatMessage[]>([]);
  let text = $state("");
  const status = hubStatus("/hubs/rooms");
  const joinRoom = hubInvoke("/hubs/rooms", "JoinRoom");
  const sendMessage = hubInvoke("/hubs/rooms", "SendMessage");

  $effect(() => {
    if ($status === "connected") joinRoom(roomId).catch(ignoreAbort);
  });

  onHubEvent("/hubs/rooms", "MessageReceived", (message) => {
    if (message.roomId === roomId) messages.push(message);
  });

  async function submit(event: SubmitEvent) {
    event.preventDefault();
    try {
      await sendMessage(roomId, text);
      text = "";
    } catch (e) {
      ignoreAbort(e);
    }
  }
</script>

<ul>
  {#each messages as message}
    <li>{message.user}: {message.text}</li>
  {/each}
</ul>
<form onsubmit={submit}>
  <input bind:value={text} />
  <button disabled={$status !== "connected"}>Send</button>
</form>

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

Next, read Client config and Connection status.

On this page