Skip to main content

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​

ValueTypeDescription
connectedProvidersEhrProvider[]List of currently connected EHR providers
addProvider(provider)functionAdd a provider to the connected list
removeProvider(name)functionRemove a provider from the connected list
getProviderForResource(resource)functionGiven 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 localStorage under the key ehr-connected-providers.
  • On page load, App.tsx fetches all active PatientEHRConnection resources from the server and syncs the context — adding providers that are active and removing ones that are no longer active.
  • getProviderForResource works by reading the identifier[].system field 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​

ValueTypeDescription
pageTitlestringThe title shown in the header
headerActionReactNodeOptional button/element shown on the right of the header
setPageTitle(title)functionSet the page title
setHeaderAction(action)functionSet 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​

ValueTypeDescription
timeoutSettingTimeoutOptionCurrent timeout duration (e.g., '15 minutes')
setTimeoutSetting(v)functionUpdate 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​

  1. 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().
  2. Listens for activity events: mousemove, mousedown, keydown, touchstart, scroll, click.
  3. Any activity resets both timers (throttled to once per second to avoid performance issues).
  4. If the patient clicks "Stay Logged In", timers are reset.

Available timeout options​

OptionDuration
'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​

ValueTypeDescription
connectedHospitalsConnectedHospital[]List of connected hospitals with name, logo, and vendor info
loadingbooleanTrue while fetching
refetch()functionManually 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​

TypeWhen it fires
ehr_expiredEHR access token expired during sync
sync_completeAll EHR providers synced successfully
sync_partialSome providers synced, some had expired tokens
sync_failedSync failed entirely
support_replySupport ticket received a reply
support_status_changedSupport ticket status was updated

What it provides​

ValueTypeDescription
notificationsAppNotification[]All notifications, newest first
unreadCountnumberCount of unread notifications
loadedbooleanTrue after initial fetch completes
refresh()functionRe-fetch from FHIR
addNotification(n, deduplicate?)functionCreate a new notification (optimistic + FHIR persist)
markRead(id)functionMark one notification as read
markAllRead()functionMark all as read
dismiss(id)functionRemove one notification (deletes FHIR resource)
clearAll()functionRemove 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​

ValueTypeDescription
mode'patient' | 'expert'Current view mode
setMode(mode)functionChange 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​

OptionTypeDescription
resourceTypestringFHIR resource type (e.g., 'Observation')
querystringFHIR search query string
patientIdstring | undefinedPatient ID — fetch only starts when defined
initialSortStatusDataTableSortStatusDefault sort column and direction
initialFilterValuesRecord<string, string[]>Default filter state

Returns​

ValueDescription
recordsAll fetched records (grows as pages stream in)
loadingTrue while fetching
page, setPageCurrent page number
pageSize, setPageSizeRows per page
sortStatus, setSortStatusCurrent sort column/direction
searchQuery, setSearchQuerySearch input value
filterValues, setFilterValues, handleFilterChangeFilter state
isMobileTrue 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​

ParamTypeDescription
itemsTranslateItemsMap of sections → items to translate (conditions, observations, medications, etc.)
patientRefstring | undefinedPatient FHIR reference (e.g., Patient/abc123)
tokenstring | undefinedBearer token for the AI service
modeViewModeCurrent view mode — only fetches when 'patient'

TranslateItem fields​

Each item in the items map has this shape:

FieldTypeDescription
idstringFHIR resource ID — used as the key in the response
codestring?Clinical code (e.g., SNOMED, LOINC code)
displaystring?Raw clinical display name (e.g., "Hypertension")
clinicalStatusstring?Status code (e.g., "active", "resolved") — for conditions
interpretationstring?Lab interpretation code (e.g., "H", "L") — for observations
valuestring?Numeric value — for observations
unitstring?Unit of measure — for observations

Available sections​

TranslateItems supports these section keys:

SectionFHIR resource
conditionsCondition
observationsObservation
medicationsMedicationRequest
allergiesAllergyIntolerance
proceduresProcedure
immunizationsImmunization
encountersEncounter
reportsDocumentReference

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 fieldDescription
section_labelsPlain-language labels for each section heading (e.g., "Your Lab Results" instead of "Observations")
translationsNested map: section → itemId → { plain_name, plain_status }
plain_namePatient-friendly name for the item (e.g., "High Blood Pressure")
plain_statusPatient-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​

ValueDescription
translationsThe full translations map from the API response
sectionLabelsThe section_labels map from the API response
loadingTrue while the API call is in-flight
showSkeletonsTrue 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​

OutcomeToastNotification type
All syncedGreen "Records refreshed"sync_complete
Some synced, some expiredYellow "Partial sync"sync_partial
All expired, none syncedOrange "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.