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.
- Solid 1.7 or later.
Install
Install the package and its SignalR peer:
npm install @dammers/use-signalr-solid @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-solid";
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-solid";
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:
import { SignalRProvider } from "./client";
render(
() => (
<SignalRProvider
baseUrl="https://api.example.com"
accessTokenFactory={getToken}
enabled={isSignedIn()}
>
<App />
</SignalRProvider>
),
document.getElementById("root")!,
);Use a hub in a component
This component shows the messages of a room and sends one message:
import { createEffect, createSignal, For } from "solid-js";
import type { ChatMessage } from "./contract";
import { useHubStatus, useSignalREffect, useSignalRInvoke } from "./client";
import { ignoreAbort } from "./ignore-abort";
export function Room(props: { roomId: string }) {
const [messages, setMessages] = createSignal<ChatMessage[]>([]);
const status = useHubStatus("/hubs/rooms");
const joinRoom = useSignalRInvoke("/hubs/rooms", "JoinRoom");
const sendMessage = useSignalRInvoke("/hubs/rooms", "SendMessage");
createEffect(() => {
if (status() === "connected") joinRoom(props.roomId).catch(ignoreAbort);
});
useSignalREffect("/hubs/rooms", "MessageReceived", (message) => {
if (message.roomId === props.roomId) setMessages((all) => [...all, message]);
});
return (
<div>
<ul>
<For each={messages()}>{(m) => <li>{m.user}: {m.text}</li>}</For>
</ul>
<button
disabled={status() !== "connected"}
onClick={() => sendMessage(props.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/solid.
Next, read Client config and Connection status.