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:

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:

  1. WebSocket Connection - There is only one WebSocket connection per MedplumClient instance, shared across all active subscriptions. This is the underlying network transport.
  2. 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:

Available Lifecycle Hooks

The lifecycle hooks are passed as part of the third parameter (options) to the useSubscription hook:

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:

  1. On Initial Connection:
    • onWebSocketOpenonSubscriptionConnect
  2. On Disconnection:
    • onSubscriptionDisconnectonWebSocketClose
  3. On Reconnection:
    • onWebSocketOpenonSubscriptionConnect

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.