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.
  • Vue 3.3 or later.

Install

Install the package and its SignalR peer:

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

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

export const signalR = createSignalRClient({ hubs });

export const {
  useSignalR,
  useHubConsumer,
  useHubStatus,
  useSignalREvent,
  useSignalRInvoke,
  useSignalRSend,
  useSignalRTeardown,
  useOnReconnected,
} = signalR;

Start the session

Give app.use(signalR, options) the base URL of your server and a function that returns the access token:

provider.ts
import { createApp, ref } from "vue";
import App from "./App.vue";
import { signalR } from "./client";

const token = ref("");

createApp(App)
  .use(signalR, {
    baseUrl: "https://api.example.com",
    accessTokenFactory: () => token.value,
    enabled: () => token.value !== "",
  })
  .mount("#app");

Use a hub in a component

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

quick-start.vue
<script setup lang="ts">
import { ref, watch } from "vue";
import { useHubStatus, useSignalREvent, useSignalRInvoke } from "./client";
import { ignoreAbort } from "./ignore-abort";
import type { ChatMessage } from "./contract";

const props = defineProps<{ roomId: string }>();
const messages = ref<ChatMessage[]>([]);
const text = ref("");
const status = useHubStatus("/hubs/rooms");
const joinRoom = useSignalRInvoke("/hubs/rooms", "JoinRoom");
const sendMessage = useSignalRInvoke("/hubs/rooms", "SendMessage");

watch(
  [status, () => props.roomId],
  ([current, roomId]) => {
    if (current === "connected") joinRoom(roomId).catch(ignoreAbort);
  },
  { immediate: true },
);

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

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

<template>
  <ul>
    <li v-for="(message, index) in messages" :key="index">{{ message.user }}: {{ message.text }}</li>
  </ul>
  <form @submit.prevent="submit">
    <input v-model="text" />
    <button :disabled="status !== 'connected'">Send</button>
  </form>
</template>

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

Next, read Client config and Connection status.

On this page