Quick start
Install the package, declare a contract, and call a hub from a component.
Requirements
- Node 20.19 or later.
@microsoft/signalr8, 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/signalrSet up
Write the server hub
The samples in this documentation use this hub:
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:
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:
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:
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:
<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.