Contexts & Hooks
The Member Portal uses React Contexts to share state across pages without prop drilling, and custom hooks to encapsulate reusable logic.
EhrContext​
File: memberportal/src/contexts/EhrContext.tsx
Tracks which EHR providers the patient has connected. This state is used by the Sidebar, Dashboard, and all health record pages to show EHR badges and filter by provider.
What it provides​
| Value | Type | Description |
|---|---|---|
connectedProviders | EhrProvider[] | List of currently connected EHR providers |
addProvider(provider) | function | Add a provider to the connected list |
removeProvider(name) | function | Remove a provider from the connected list |
getProviderForResource(resource) | function | Given a FHIR resource, return which EHR it came from |
getResourceProvider(resourceId) | function | (Deprecated) Hash-based provider lookup |
How it works​
- Connected providers are stored in
localStorageunder the keyehr-connected-providers. - On page load,
App.tsxfetches all activePatientEHRConnectionresources from the server and syncs the context — adding providers that are active and removing ones that are no longer active. getProviderForResourceworks by reading theidentifier[].systemfield on the FHIR resource and matching it against known EHR prefixes (urn:epic:,urn:healow:,urn:meditech:).
How to use it in a page​
import { useEhrContext } from '../contexts/EhrContext';
const { connectedProviders, getProviderForResource } = useEhrContext();
// Show which EHR a resource came from
const provider = getProviderForResource(someObservation);
HeaderContext​
File: memberportal/src/contexts/HeaderContext.tsx
Lets any page dynamically control what appears in the top header bar — the page title and an optional action button on the right.
What it provides​
| Value | Type | Description |
|---|---|---|
pageTitle | string | The title shown in the header |
headerAction | ReactNode | Optional button/element shown on the right of the header |
setPageTitle(title) | function | Set the page title |
setHeaderAction(action) | function | Set the header action element |
How to use it in a page​
Every page that wants a title in the header does this:
import { useHeader } from '../contexts/HeaderContext';
const { setPageTitle, setHeaderAction } = useHeader();
useEffect(() => {
setPageTitle('Lab Results');
setHeaderAction(<ChatButton />);
// Clean up when leaving the page
return () => {
setPageTitle('');
setHeaderAction(null);
};
}, []);
IdleTimeoutContext​
File: memberportal/src/contexts/IdleTimeoutContext.tsx
Stores the patient's selected session timeout duration and makes it available to both SettingsPage (to display and change it) and IdleTimeoutGuard (to enforce it).
What it provides​
| Value | Type | Description |
|---|---|---|
timeoutSetting | TimeoutOption | Current timeout duration (e.g., '15 minutes') |
setTimeoutSetting(v) | function | Update the timeout duration |
useIdleTimeout​
File: memberportal/src/hooks/useIdleTimeout.ts
The core logic for idle session management. Monitors user activity and triggers a logout after inactivity.
How it works​
- Sets two timers when called:
- Warning timer — fires 2 minutes before the timeout, shows the warning modal and starts a countdown.
- Logout timer — fires at the exact timeout duration and calls
onLogout().
- Listens for activity events:
mousemove,mousedown,keydown,touchstart,scroll,click. - Any activity resets both timers (throttled to once per second to avoid performance issues).
- If the patient clicks "Stay Logged In", timers are reset.
Available timeout options​
| Option | Duration |
|---|---|
'15 minutes' | 900,000 ms |
'30 minutes' | 1,800,000 ms |
'60 minutes' | 3,600,000 ms |
'4 hours' | 14,400,000 ms |
HospitalContext​
File: memberportal/src/contexts/HospitalContext.tsx
Tracks which individual hospitals (as opposed to EHR vendors) the patient has connected. Each hospital connection points to a FHIR Endpoint resource that holds the hospital name, logo URL, and EHR vendor type.
This is distinct from EhrContext — EhrContext tracks vendor-level connections (Epic, Healow, MEDITECH), while HospitalContext tracks hospital-level connections where the ehrProvider field is a UUID pointing to an Endpoint.
What it provides​
| Value | Type | Description |
|---|---|---|
connectedHospitals | ConnectedHospital[] | List of connected hospitals with name, logo, and vendor info |
loading | boolean | True while fetching |
refetch() | function | Manually re-fetch the hospital list |
How to use it​
import { useHospitalContext } from '../contexts/HospitalContext';
const { connectedHospitals, loading, refetch } = useHospitalContext();
NotificationContext​
File: memberportal/src/contexts/NotificationContext.tsx
Manages the patient's in-app notification feed. Notifications are stored as PatientNotification FHIR resources in Medplum, so they persist across sessions. The context loads them on login and provides full CRUD operations.
Notification types​
| Type | When it fires |
|---|---|
ehr_expired | EHR access token expired during sync |
sync_complete | All EHR providers synced successfully |
sync_partial | Some providers synced, some had expired tokens |
sync_failed | Sync failed entirely |
support_reply | Support ticket received a reply |
support_status_changed | Support ticket status was updated |
What it provides​
| Value | Type | Description |
|---|---|---|
notifications | AppNotification[] | All notifications, newest first |
unreadCount | number | Count of unread notifications |
loaded | boolean | True after initial fetch completes |
refresh() | function | Re-fetch from FHIR |
addNotification(n, deduplicate?) | function | Create a new notification (optimistic + FHIR persist) |
markRead(id) | function | Mark one notification as read |
markAllRead() | function | Mark all as read |
dismiss(id) | function | Remove one notification (deletes FHIR resource) |
clearAll() | function | Remove all notifications |
How to use it​
import { useNotifications } from '../contexts/NotificationContext';
const { notifications, unreadCount, markRead } = useNotifications();
PatientViewContext​
File: memberportal/src/contexts/PatientViewContext.tsx
Controls whether the portal is in patient mode or expert mode.
- Patient mode — medical terms are translated into plain language via
usePatientTranslate. Designed for patients without clinical backgrounds. - Expert mode — raw clinical data is shown as-is. Designed for clinicians or tech-savvy users who want the original FHIR values.
The selected mode is persisted in both localStorage (for instant load without flicker) and as a FHIR extension on the Patient resource (http://chartchathealth.com/fhir/StructureDefinition/view-mode), so it syncs across devices.
What it provides​
| Value | Type | Description |
|---|---|---|
mode | 'patient' | 'expert' | Current view mode |
setMode(mode) | function | Change the view mode |
How to use it​
import { usePatientView } from '../contexts/PatientViewContext';
const { mode, setMode } = usePatientView();
useFhirPagedData​
File: memberportal/src/hooks/useFhirPagedData.ts
The primary data-fetching hook used by all list pages (Lab Results, Medications, Conditions, Visits, etc.). It handles FHIR pagination, sorting, filtering, search, and mobile responsiveness in one place.
Internally it uses medplum.searchResourcePages() to stream all pages of a FHIR search into records[] progressively — the table fills in as data arrives rather than waiting for the full set.
Options​
| Option | Type | Description |
|---|---|---|
resourceType | string | FHIR resource type (e.g., 'Observation') |
query | string | FHIR search query string |
patientId | string | undefined | Patient ID — fetch only starts when defined |
initialSortStatus | DataTableSortStatus | Default sort column and direction |
initialFilterValues | Record<string, string[]> | Default filter state |
Returns​
| Value | Description |
|---|---|
records | All fetched records (grows as pages stream in) |
loading | True while fetching |
page, setPage | Current page number |
pageSize, setPageSize | Rows per page |
sortStatus, setSortStatus | Current sort column/direction |
searchQuery, setSearchQuery | Search input value |
filterValues, setFilterValues, handleFilterChange | Filter state |
isMobile | True on screens < 700px |
tableHeight(rowCount) | Calculates a dynamic CSS height string for the table |
paginate(sorted) | Slices a sorted array to the current page |
Usage pattern​
const { records, loading, page, setPage, pageSize, setPageSize,
sortStatus, setSortStatus, searchQuery, setSearchQuery,
filterValues, handleFilterChange, isMobile, tableHeight, paginate }
= useFhirPagedData<Observation>({
resourceType: 'Observation',
query: `subject=Patient/${patientId}&category=laboratory`,
patientId,
initialSortStatus: { columnAccessor: 'date', direction: 'desc' },
});
usePatientTranslate​
File: memberportal/src/hooks/usePatientTranslate.ts
Calls the AI service (VITE_CHAT_API_URL/api/patient-translate) to translate clinical terms into plain patient-friendly language. Only runs in patient mode — in expert mode it returns empty results immediately.
Results are cached in sessionStorage keyed by patient reference + item IDs, so switching between pages doesn't re-trigger the API call for the same data.
Inputs​
| Param | Type | Description |
|---|---|---|
items | TranslateItems | Map of sections → items to translate (conditions, observations, medications, etc.) |
patientRef | string | undefined | Patient FHIR reference (e.g., Patient/abc123) |
token | string | undefined | Bearer token for the AI service |
mode | ViewMode | Current view mode — only fetches when 'patient' |
TranslateItem fields​
Each item in the items map has this shape:
| Field | Type | Description |
|---|---|---|
id | string | FHIR resource ID — used as the key in the response |
code | string? | Clinical code (e.g., SNOMED, LOINC code) |
display | string? | Raw clinical display name (e.g., "Hypertension") |
clinicalStatus | string? | Status code (e.g., "active", "resolved") — for conditions |
interpretation | string? | Lab interpretation code (e.g., "H", "L") — for observations |
value | string? | Numeric value — for observations |
unit | string? | Unit of measure — for observations |
Available sections​
TranslateItems supports these section keys:
| Section | FHIR resource |
|---|---|
conditions | Condition |
observations | Observation |
medications | MedicationRequest |
allergies | AllergyIntolerance |
procedures | Procedure |
immunizations | Immunization |
encounters | Encounter |
reports | DocumentReference |
API request​
Endpoint: POST {VITE_CHAT_API_URL}/api/patient-translate
Headers:
Content-Type: application/json
Authorization: Bearer <medplum_access_token>
Body:
{
"items": {
"conditions": [
{ "id": "abc123", "code": "38341003", "display": "Hypertension", "clinicalStatus": "active" }
],
"observations": [
{ "id": "def456", "code": "2339-0", "display": "Glucose [Mass/volume] in Blood", "value": "210", "unit": "mg/dL", "interpretation": "H" }
]
}
}
Only sections with at least one item are included in the request body — empty sections are filtered out.
API response​
{
"section_labels": {
"conditions": "Your Health Conditions",
"observations": "Your Lab Results"
},
"translations": {
"conditions": {
"abc123": {
"plain_name": "High Blood Pressure",
"plain_status": "Ongoing"
}
},
"observations": {
"def456": {
"plain_name": "Blood Sugar Level",
"plain_status": "High"
}
}
}
}
| Response field | Description |
|---|---|
section_labels | Plain-language labels for each section heading (e.g., "Your Lab Results" instead of "Observations") |
translations | Nested map: section → itemId → { plain_name, plain_status } |
plain_name | Patient-friendly name for the item (e.g., "High Blood Pressure") |
plain_status | Patient-friendly status (e.g., "Ongoing", "Resolved", "High") |
Caching​
Results are cached in sessionStorage under the key pt_translate_{patientRef}_{sortedItemIds}. The cache key changes only when the set of item IDs changes, so navigating away and back to a page serves translations instantly from cache without a new API call.
Returns​
| Value | Description |
|---|---|
translations | The full translations map from the API response |
sectionLabels | The section_labels map from the API response |
loading | True while the API call is in-flight |
showSkeletons | True when in patient mode, items exist, but translations haven't arrived yet — use this (not loading) to render skeleton placeholders |
Usage pattern​
const { translations, showSkeletons } = usePatientTranslate(
{ conditions: conditionItems },
`Patient/${patientId}`,
medplum.getAccessToken(),
mode
);
// In render:
<TranslatedText
showSkeleton={showSkeletons}
value={translations['conditions']?.[condition.id]?.plain_name}
fallback={condition.code?.text ?? 'Unknown'}
/>
useSyncPoller​
File: memberportal/src/hooks/useSyncPoller.ts
Polls the EHR sync status endpoint every 3 seconds after a sync is triggered and fires both Mantine toast notifications and persistent bell notifications based on the result.
Returns a single pollSync(token, callbacks?) function. Any page that triggers an EHR sync calls this to handle the result consistently.
Poll outcomes​
| Outcome | Toast | Notification type |
|---|---|---|
| All synced | Green "Records refreshed" | sync_complete |
| Some synced, some expired | Yellow "Partial sync" | sync_partial |
| All expired, none synced | Orange "EHR session expired" | ehr_expired |
Usage​
const { pollSync } = useSyncPoller();
// After triggering sync:
pollSync(accessToken, {
onDone: () => setRefreshing(false),
onSuccess: () => refetchRecords(),
});
useAuditLog​
File: memberportal/src/hooks/useAuditLog.ts
A hook for writing audit log entries. Used by the Chat Assistant to record when a patient views or interacts with health data, for compliance purposes.
useTheme​
File: memberportal/src/hooks/useTheme.ts
A hook for reading the current theme (light/dark). Used by components that need to adapt their styling based on the active theme.