useSubscription Hook
useSubscription creates an in-memory Subscription resource on the Medplum server with the given criteria and calls the
given callback when an event notification is triggered.
Subscriptions created with this hook are lightweight, share a single WebSocket connection per instance of MedplumClient, and are automatically
untracked and cleaned up when the containing component is no longer mounted.
Use Cases
The useSubscription hook is a powerful tool for creating applications that require the client to wait for events asynchronously from the server.
Some examples include:
- Live chat
- Notification badges for tasks
- Realtime analytics dashboards
The hook makes it extremely simple to listen for resource interactions that satisfy a specified criteria and act on them reactively, rather than having to poll via expensive search requests on a timer.
Usage
The useSubscription hook takes a FHIR search criteria and a callback function to call when a resource interaction
that satisfies the Subscription occurs.
function MyComponent(): JSX.Element {
const [notificationCount, setNotificationCount] = useState(0);
useSubscription('Communication?sender=Practitioner/abc-123&recipient=Practitioner/me-456', (bundle: Bundle) => {
console.log('Received a message from Practitioner/abc-123!');
handleNotificationBundle(bundle); // Do something with the bundle
setNotificationCount((s) => s + 1);
});
return <div>Notifications received: {notificationCount}</div>;
}
Parsing the Subscription Bundle
The callback receives a Bundle which contains a SubscriptionStatus as its first entry (bundle.entry[0].resource)
and the resource for which the Subscription fired for in its second entry (bundle.entry[1].resource):
{
"id": "90154e8b-e283-4973-a562-1b0e08611260",
"resourceType": "Bundle",
"type": "history",
"timestamp": "2024-10-29T23:52:53.282Z",
"entry": [
// The `SubscriptionStatus` resource, which tells us which `Subscription` this event notification is for
{
"resource": {
"id": "d5c532d0-cc4c-4c5b-8212-e22dcea22c73",
"resourceType": "SubscriptionStatus",
"status": "active",
"type": "event-notification",
"subscription": {
"reference": "Subscription/90ab8fc7-d8cf-447b-9451-9259846f71e4" // Here is the Subscription reference
},
"notificationEvent": [
{
"eventNumber": "0",
"timestamp": "2024-10-29T23:52:53.282Z",
"focus": {
"reference": "Communication/54d902bb-a8e6-4f60-a671-64591169aa5b" // Here is a reference to the resource below
}
}
]
}
},
// The actual `Communication` resource this event fired for
{
"resource": {
"resourceType": "Communication",
"status": "in-progress",
"payload": [{ "contentString": "Hello, Medplum!" }],
"sent": "2024-10-29T23:52:53.240Z",
"id": "54d902bb-a8e6-4f60-a671-64591169aa5b"
},
"fullUrl": "https://api.medplum.com/fhir/R4/Communication/54d902bb-a8e6-4f60-a671-64591169aa5b"
}
]
}
So you can parse the status and the resource from the Bundle like this:
function handleNotificationBundle(bundle: Bundle): void {
// The first entry is the status, which contains a reference to the `Subscription` this notification is for
const status = bundle.entry?.[0]?.resource as SubscriptionStatus;
console.log('Received subscription status: ', status);
// The second entry is the actual resource
const communication = bundle.entry?.[1]?.resource as Communication;
console.log('Received communication: ', communication);
}
Dynamic Criteria
Changing the criteria string will automatically decrease the reference count for the current Subscription resource and create a new Subscription
with the new criteria.
function MyComponent(): JSX.Element {
const profile = useMedplumProfile();
const [notificationCount, setNotificationCount] = useState(0);
// We can track the communications for the current user only
const profileStr = useMemo<string>(() => getReferenceString(profile), [profile]);
useSubscription(`Communication?sender=Practitioner/abc-123&recipient=${profileStr}`, (bundle: Bundle) => {
console.log('Received a message from Practitioner/abc-123!');
handleNotificationBundle(bundle); // Do something with the bundle
setNotificationCount((s) => s + 1);
});
return <div>Notifications received: {notificationCount}</div>;
}
Temporarily Unsubscribing
In the case of wanting to temporarily unsubscribe from the current criteria until some condition has been met (for example, waiting for a search to return or a profile to refresh),
you can pass an empty string as the criteria string and the previous Subscription will be cleaned up without creating a new Subscription
until the criteria string has been changed again.
function MyComponent(): JSX.Element {
const profile = useMedplumProfile();
const [notificationCount, setNotificationCount] = useState(0);
// We can track the communications for the current user only
const profileStr = useMemo<string>(() => (profile ? getReferenceString(profile) : ''), [profile]);
useSubscription(
// When profileStr is `undefined` we can pass an empty string to temporarily unsubscribe from any criteria
profileStr ? `Communication?sender=Practitioner/abc-123&recipient=${profileStr}` : '',
(bundle: Bundle) => {
console.log('Received a message from Practitioner/abc-123!');
handleNotificationBundle(bundle); // Do something with the bundle
setNotificationCount((s) => s + 1);
}
);
return <div>Notifications received: {notificationCount}</div>;
}
Usage within an Expo app
Usage within Expo / React Native has some special considerations. See: @medplum/expo-polyfills README
Examples
Subscription Extensions
Any Subscription extension supported by Medplum can be attached to a Subscription
created by the useSubscription hook via a 3rd optional parameter to the hook, options, which takes an optional subscriptionProps.
type UseSubscriptionOptions = {
subscriptionProps?: Partial<Subscription>;
};
Here's how you would subscribe to only create interactions for a criteria:
const createOnlyOptions = {
subscriptionProps: {
extension: [
{
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
valueCode: 'create',
},
],
},
};
function MyComponent(): JSX.Element {
const [createCount, setCreateCount] = useState(0);
useSubscription(
'Communication?sender=Practitioner/abc-123&recipient=Practitioner/me-456',
(_bundle) => {
console.log('Received a new message from Practitioner/abc-123!');
setCreateCount((s) => s + 1);
},
createOnlyOptions
);
return <div>Create notifications received: {createCount}</div>;
}
Subscriptions with the same criteria are tracked separately if they have differing subscriptionProps. This means you can create one Subscription
to listen for create interactions and another for update interactions and they will not interfere with each other.
const createOnlyOptions = {
subscriptionProps: {
extension: [
{
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
valueCode: 'create',
},
],
},
};
const updateOnlyOptions = {
subscriptionProps: {
extension: [
{
url: 'https://medplum.com/fhir/StructureDefinition/subscription-supported-interaction',
valueCode: 'update',
},
],
},
};
function MyComponent(): JSX.Element {
const [createCount, setCreateCount] = useState(0);
const [updateCount, setUpdateCount] = useState(0);
useSubscription(
'Communication?sender=Practitioner/abc-123&recipient=Practitioner/me-456',
(_bundle) => {
console.log('Received a new message from Practitioner/abc-123!');
setCreateCount((s) => s + 1);
},
createOnlyOptions
);
useSubscription(
'Communication?sender=Practitioner/abc-123&recipient=Practitioner/me-456',
(_bundle) => {
console.log('Received an update to message from Practitioner/abc-123!');
setUpdateCount((s) => s + 1);
},
updateOnlyOptions
);
return (
<>
<div>Create notifications received: {createCount}</div>
<div>Update notifications received: {updateCount}</div>
</>
);
}
Lifecycle Hooks
The useSubscription hook provides lifecycle callbacks that allow you to respond to connection state changes. These are particularly useful for managing reconnection scenarios, especially in mobile applications where the WebSocket connection may be interrupted due to app backgrounding or network changes.
Two Concepts: WebSocket Connection vs. Subscription
It's important to understand that there are two distinct layers at play:
- WebSocket Connection - There is only one WebSocket connection per
MedplumClientinstance, shared across all active subscriptions. This is the underlying network transport. - Subscriptions - Multiple subscriptions can run simultaneously on the same WebSocket connection. Each subscription represents a specific query/filter for resource changes.
The lifecycle hooks allow you to respond to changes at both layers:
onWebSocketOpen/onWebSocketClose- Fired once when the shared WebSocket connection opens or closes (affects all subscriptions)onSubscriptionConnect/onSubscriptionDisconnect- Fired individually for each subscription when that specific subscription is established or disconnected
Available Lifecycle Hooks
The lifecycle hooks are passed as part of the third parameter (options) to the useSubscription hook:
onWebSocketOpen- Called when the underlying WebSocket connection is successfully establishedonWebSocketClose- Called when the WebSocket connection is closed (e.g., due to network issues or app backgrounding)onSubscriptionConnect- Called when this specific subscription has been established on the server and is actively receiving notificationsonSubscriptionDisconnect- Called when this specific subscription is disconnected and is no longer receiving notifications
Why Use Lifecycle Hooks?
Handling Missed Events During Reconnection
When a WebSocket connection is interrupted (especially common in mobile apps when backgrounding), your application will miss any resource changes that occur while disconnected. The lifecycle hooks allow you to implement a "catch-up" mechanism to fetch missed events after reconnecting.
For example, in a React Native/Expo app, the WebSocket automatically closes when the app is backgrounded for stability reasons, and automatically reconnects when the app becomes active again. Without lifecycle hooks, you would miss notifications that occurred during this disconnection period.
Providing User Feedback
The lifecycle hooks enable you to show connection status to users (e.g., "Disconnected", "Reconnecting..."), which is important for applications where real-time updates are critical, such as live chat or task notifications.
Example: Catching Up on Missed Events
Here's an example showing how to use lifecycle hooks to handle reconnection and fetch missed events:
import type { Bundle } from '@medplum/fhirtypes';
import { useMedplum, useSubscription } from '@medplum/react-hooks';
import type { ReactNode } from 'react';
import { useCallback, useState } from 'react';
function LiveChatComponent(): ReactNode {
const medplum = useMedplum();
const [isConnected, setIsConnected] = useState(true);
useSubscription(
'Communication',
(_bundle: Bundle) => {
// Handle incoming messages in real-time
},
{
// Show connection status
onWebSocketOpen: useCallback(() => {
setIsConnected(true);
}, []),
onWebSocketClose: useCallback(() => {
setIsConnected(false);
}, []),
// Refresh chat state when subscription reconnects
onSubscriptionConnect: useCallback(() => {
medplum.searchResources('Communication', { _sort: '-_lastUpdated' });
}, [medplum]),
}
);
return (
<div>
<div>{isConnected ? '🟢 Connected' : '🔴 Disconnected'}</div>
</div>
);
}
Hook Invocation Order
Understanding when each hook is called helps you implement the correct logic:
-
On Initial Connection:
onWebSocketOpen→onSubscriptionConnect
-
On Disconnection:
onSubscriptionDisconnect→onWebSocketClose
-
On Reconnection:
onWebSocketOpen→onSubscriptionConnect
Note that onWebSocketOpen/onWebSocketClose are called once for the shared WebSocket connection (used by all subscriptions), while onSubscriptionConnect/onSubscriptionDisconnect are called individually for each specific subscription criteria.
Troubleshooting
Error: WebSocket subscriptions not enabled for current project
Currently the WebSocket Subscription feature which is required to use the useSubscription hook is behind a feature flag.
Locally, you can enable this feature flag by logging in as a super admin
and enabling the websocket-subscriptions feature on your Project resource from @medplum/app.
To get this feature enabled for your project on hosted Medplum (app.medplum.com), send an email to hello@medplum.com.