Learnixo
Back to blog
AI Systemsintermediate

SignalR JavaScript Client — Connecting, Reconnecting, and Handling Events

Use the @microsoft/signalr JavaScript client: connection lifecycle, automatic reconnection, invoking hub methods, handling disconnects, and the patterns for a resilient clinical dashboard frontend.

Asma Hafeez KhanMay 16, 20265 min read
SignalRJavaScriptTypeScriptReal-Time.NET
Share:𝕏

Installation

Bash
# npm
npm install @microsoft/signalr

# CDN (for non-module environments)
# <script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr/dist/browser/signalr.min.js"></script>

Building a Connection

TYPESCRIPT
import * as signalR from "@microsoft/signalr";

const connection = new signalR.HubConnectionBuilder()
    // Hub URL — relative or absolute
    .withUrl("/hubs/clinical", {
        // JWT token factory — called on initial connect and reconnect
        accessTokenFactory: () => getAccessToken(),

        // Transport preference (falls back automatically)
        transport: signalR.HttpTransportType.WebSockets,

        // Headers for long-polling fallback
        headers: { "X-Custom-Header": "value" },
    })

    // Automatic reconnection with custom delays (ms)
    .withAutomaticReconnect([0, 2000, 10000, 30000, null])
    // null = stop retrying, positive numbers = wait that long between retries

    // Log level — reduce in production
    .configureLogging(signalR.LogLevel.Warning)

    .build();

Starting the Connection

TYPESCRIPT
async function startConnection(): Promise<void> {
    try {
        await connection.start();
        console.log("Connected to clinical hub");
        await onConnected();
    } catch (err) {
        console.error("Connection failed:", err);
        // Manual retry if withAutomaticReconnect is not sufficient
        setTimeout(startConnection, 5000);
    }
}

async function onConnected(): Promise<void> {
    // Subscribe to ward updates after connecting
    await connection.invoke("SubscribeToWard", currentWardId);
}

startConnection();

Registering Event Handlers

TYPESCRIPT
// Register BEFORE starting the connection
// Handlers registered after start() may miss early messages

// Hub sends PatientAdmitted → call this function
connection.on("PatientAdmitted", (patient: PatientAdmittedDto) => {
    updatePatientList(patient);
    showNotification(`${patient.name} admitted to ${patient.ward}`);
});

connection.on("DrugOrderStatusChanged", (status: DrugOrderStatusDto) => {
    updateOrderStatus(status.orderId, status.newStatus);
});

connection.on("AlarmTriggered", (alarm: AlarmDto) => {
    playAlertSound(alarm.severity);
    showAlarmModal(alarm);
});

connection.on("WardCapacityUpdated", (capacity: WardCapacityDto) => {
    updateCapacityDisplay(capacity.occupied, capacity.total);
});

// Remove a specific handler
const handler = (patient: PatientAdmittedDto) => updatePatientList(patient);
connection.on("PatientAdmitted", handler);
// Later:
connection.off("PatientAdmitted", handler);

// Remove all handlers for an event
connection.off("PatientAdmitted");

Invoking Hub Methods

TYPESCRIPT
// invoke — call hub method, wait for return value
const capacity = await connection.invoke<WardCapacityDto>(
    "GetWardCapacity", wardId);
console.log(`Beds: ${capacity.occupied}/${capacity.total}`);

// send — call hub method, do not wait for return value
await connection.send("AcknowledgeAlarm", alarmId);

// invoke with error handling
try {
    await connection.invoke("OrderMedication", patientId, drugName);
} catch (err: unknown) {
    if (err instanceof Error) {
        // HubException messages are forwarded here
        showError(`Medication order failed: ${err.message}`);
    }
}

Reconnection Lifecycle

TYPESCRIPT
// Called while attempting to reconnect
connection.onreconnecting((error) => {
    console.warn("Connection lost, reconnecting...", error);
    updateConnectionStatus("reconnecting");
    disableInteractiveControls();
});

// Called when reconnect succeeds
connection.onreconnected(async (connectionId) => {
    console.log("Reconnected:", connectionId);
    updateConnectionStatus("connected");
    enableInteractiveControls();

    // Critical: re-join groups — group membership is per-connection
    await connection.invoke("SubscribeToWard", currentWardId);
    await connection.invoke("SubscribeToAlerts", subscribedAlerts);

    // Reload any state that may have changed during disconnect
    await refreshWardState();
});

// Called when reconnect gives up (after all retry intervals exhausted)
connection.onclose((error) => {
    console.error("Connection permanently closed:", error);
    updateConnectionStatus("disconnected");
    showPermanentDisconnectWarning();
    // Optionally: full page reload, or manual "reconnect" button
});

Streaming from the Server

TYPESCRIPT
// Receive a stream of data from the hub
const stream = connection.stream<VitalSignDto>("StreamVitalSigns", patientId);

const subscription = stream.subscribe({
    next:  (vital: VitalSignDto) => updateVitalsDisplay(vital),
    error: (err: Error) => {
        console.error("Vitals stream error:", err);
        showStreamError();
    },
    complete: () => {
        console.log("Vitals stream completed");
    },
});

// Stop the stream when the component unmounts
function cleanup(): void {
    subscription.dispose();  // cancels the CancellationToken on the server
}

Sending a Stream to the Server

TYPESCRIPT
import { Subject } from "@microsoft/signalr";

// Client streams data to the server
const subject = new Subject<ObservationDto>();

await connection.send("UploadObservations", subject);

// Send observations
for (const obs of observations) {
    subject.next(obs);
    await delay(100);
}

subject.complete();  // signals end of stream to server

TypeScript Types for Hub Events

TYPESCRIPT
// Match these to your server-side DTOs
interface PatientAdmittedDto {
    id:         string;
    name:       string;
    mrn:        string;
    ward:       string;
    admittedAt: string;  // ISO 8601 datetime
}

interface DrugOrderStatusDto {
    orderId:   string;
    newStatus: "Pending" | "Dispensing" | "Dispensed" | "Cancelled";
    updatedAt: string;
    updatedBy: string;
}

interface AlarmDto {
    id:       string;
    type:     string;
    severity: "Low" | "Medium" | "High" | "Critical";
    message:  string;
    wardId:   string;
    firedAt:  string;
}

React Integration Pattern

TYPESCRIPT
// Custom hook for ward monitoring
function useWardMonitor(wardId: string) {
    const [patients, setPatients]   = useState<PatientAdmittedDto[]>([]);
    const [capacity, setCapacity]   = useState<WardCapacityDto | null>(null);
    const [status, setStatus]       = useState<"connecting" | "connected" | "disconnected">("connecting");
    const connectionRef             = useRef<signalR.HubConnection | null>(null);

    useEffect(() => {
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/hubs/clinical", { accessTokenFactory: getAccessToken })
            .withAutomaticReconnect()
            .build();

        connection.on("PatientAdmitted",      p => setPatients(prev => [...prev, p]));
        connection.on("WardCapacityUpdated",  c => setCapacity(c));
        connection.onreconnected(async () => {
            await connection.invoke("SubscribeToWard", wardId);
        });

        connection.start().then(async () => {
            setStatus("connected");
            await connection.invoke("SubscribeToWard", wardId);
        });

        connectionRef.current = connection;
        return () => { connection.stop(); };
    }, [wardId]);

    return { patients, capacity, status };
}

Production issue I've seen: A clinical dashboard's React component registered connection.on("PatientAdmitted", ...) inside a useEffect without cleanup. When the ward changed, the old handler was never removed. After navigating between wards 5 times, 5 duplicate handlers fired per event — the same patient appeared 5 times in the list per admission. Always connection.off(...) or connection.stop() in the useEffect cleanup.


Key Takeaway

Register event handlers BEFORE calling connection.start(). Always handle onreconnecting, onreconnected, and onclose — re-join groups in onreconnected. Use invoke for hub methods that return a value; send for fire-and-forget calls. Dispose stream subscriptions to cancel server-side CancellationToken. In React, always clean up the connection in useEffect cleanup to prevent handler accumulation.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.