useSubscription Hook | Medplum
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.
Access Policy Requirements
Users must have permission to create Subscription resources and read access to the subscribed resource types. See WebSocket Subscriptions access policy for details.
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 {
const status = bundle.entry?.[0]?.resource as SubscriptionStatus;
console.log('Received subscription status: ', status);
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);
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, 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);
const profileStr = useMemo<string>(() => (profile ? getReferenceString(profile) : ''), [profile]);
useSubscription(
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, 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.
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
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.