React adapter
npm install @apextelemed/survey-core @apextelemed/survey-reactreact 18 or later is a peer dependency. The hook is built on useSyncExternalStore. The package is headless: it renders nothing and ships no CSS.
useSurveyV2Flow(options)
Section titled “useSurveyV2Flow(options)”import { useSurveyV2Flow } from '@apextelemed/survey-react';
const flow = useSurveyV2Flow({ apiBaseUrl: 'https://apextelemed.com/api', publishableKey: 'pk_0123456789abcdef0123456789abcdef', drugIds: ['drug-A'],});Options (UseSurveyV2FlowOptions)
Section titled “Options (UseSurveyV2FlowOptions)”| Option | Type | Notes |
|---|---|---|
apiBaseUrl |
string |
Required. https://apextelemed.com/api, or your proxy base. Trailing slashes are trimmed. |
publishableKey |
string |
pk_* for browser-direct mode. Omit when proxying. |
tenantKey |
string |
Sent as X-Tenant-Key in proxy mode. |
drugIds |
string[] |
Drugs to compose the survey for. Required unless token is set. |
templateId |
string |
Optional partner survey template. |
mode |
'initial' | 'refill' |
Not sent unless set. The server treats a missing mode as initial. |
token |
string |
Returning-member token. Overrides drugIds, templateId, and mode. |
skipPatientInfoWhenComplete |
boolean |
Opt in to auto-submit at the end of the questionnaire when identity is already complete. Default false. See Returning customers. |
knownPatientInfo |
Partial<PatientInfo> |
Seeds patient info for a signed-in customer on the drugIds path. |
requiredPatientInfoFields |
Array<keyof PatientInfo> |
Fields that must all be present for the skip to fire. Default: firstName, lastName, email, dob, state. |
draftTtlMs |
number |
Draft time-to-live in milliseconds. Default 24 hours. 0 disables resume. |
onEvent |
(e: EmbedEvent) => void |
Lifecycle events. See Lifecycle events. |
onComplete |
(r: V2SubmitResult) => void |
Fired once on a successful submit. |
onError |
(err: Error) => void |
Fired when the survey fails to load or to submit. |
The React hook does not expose the draftStore option. To keep drafts on your own server, build the engine from the core directly. See Custom draft store in React.
Returned SurveyV2Flow
Section titled “Returned SurveyV2Flow”All values are plain (not functions) and update on re-render.
| Field | Type | Description |
|---|---|---|
state |
SurveyV2State |
Raw engine snapshot. See Engine state. |
outcome |
SurveyV2Outcome |
Discriminated union on .kind. See below. |
flatSteps |
FlatStep[] |
All steps (one per section and step pair with at least one question), in order. |
stepIndex |
number |
Index of the current step in flatSteps. |
totalSteps |
number |
flatSteps.length. |
currentStep |
FlatStep | null |
The active step. null before the survey loads. |
visibleQuestions |
V2Question[] |
Questions on the current step whose visibility conditions match the current answers. |
answers |
Record<string, unknown> |
Answer map keyed by questionId. |
setAnswer |
(questionId: string, value: unknown) => void |
Record an answer. Clears validationError and saves the draft. |
patientInfo |
PatientInfo |
Collected patient fields. |
setPatientInfo |
(patch: Partial<PatientInfo>) => void |
Merge patient fields. Clears validationError. |
submitting |
boolean |
A next() qualification check is in flight. During submit() the phase itself becomes submitting (see outcome.kind) and this flag stays false. |
validationError |
string | null |
Current client-side validation message. |
next |
() => Promise<void> |
Validate required visible questions, run the qualification check, and advance. |
back |
() => void |
Go to the previous visible step, or from the patient-info form back to the questions. |
submit |
() => Promise<void> |
Validate patient info and submit. Only acts in the patient_info phase. |
restart |
() => void |
Clear the draft and answers and return to the first step. Keeps patient info. |
engine |
SurveyV2Engine |
The underlying engine, for advanced use. |
outcome.kind
Section titled “outcome.kind”kind |
Engine phase | Extra fields |
|---|---|---|
loading |
loading |
|
load_failed |
error |
error: string |
questions |
questions |
|
patient_info |
patient_info |
|
submitting |
submitting |
|
disqualified |
disqualified |
drugResults: V2DrugResult[] |
complete |
complete |
result: V2SubmitResult |
Lifecycle events
Section titled “Lifecycle events”Each event passed to onEvent is { type, data? }. The patient-info form counts as a pseudo-step at stepIndex === stepCount.
type |
data |
When |
|---|---|---|
survey:loaded |
{ resumed: boolean } |
The survey composed successfully. resumed is true when a draft was restored. |
step:shown |
{ stepIndex, stepCount, phase } |
A step is displayed: on load, after next(), after back(), and when the patient-info form appears (phase: 'patient_info'). |
step:completed |
{ stepIndex, stepCount } |
next() passed validation and the qualification check for the step. |
qualification:checked |
{ qualified, drugResults } |
Every qualification check, whether or not it disqualified. |
disqualified |
{ drugResults } |
Every requested drug failed the check. |
submit:succeeded |
{ responseId } |
The response was accepted. |
submit:failed |
{ error: string } |
The submit call failed. |
abandoned |
{ phase, stepIndex, stepCount } |
The page is being hidden (pagehide) while the survey is in progress. Can fire more than once per session. Forward it with fetch(..., { keepalive: true }) so it survives the unload. |
The adapter re-exports the core types you need when rendering, so a React integration can import types from @apextelemed/survey-react alone: SurveyV2State, SurveyV2Phase, FlatStep, PatientInfo, V2Question, V2QuestionOption, V2Section, V2Step, V2ComposedSurvey, V2DrugResult, V2QualificationResult, V2SubmitResult, EmbedEvent, and EmbedEventType, plus the adapter’s own UseSurveyV2FlowOptions, SurveyV2Flow, and SurveyV2Outcome.
Runtime helpers such as isQuestionVisible, qualifiedDrugIds, and disqualificationReasonText are not re-exported by the React package. Import them from @apextelemed/survey-core. See Other exports.
Rendering each phase
Section titled “Rendering each phase”Getting started shows a complete component, and examples/react-vite is a runnable app. The points that matter:
- Questions. Render
flow.visibleQuestionsforflow.currentStep. Show the option label asoption.text ?? option.valueand submitoption.value. Wire Next toflow.nextand Back toflow.back.next()refuses to advance while a required visible question is unanswered and setsvalidationErrorinstead. - Patient info. The required fields validated before submit are
firstName,lastName,email(must look like an address),dob(YYYY-MM-DD), andstate(two-letter code). Missing fields produce avalidationErrornaming them. - Disqualified.
flow.outcome.drugResultslists each drug withqualifiedand its reasons. UsedisqualificationReasonTextfrom the core to get a display string. Offerflow.restartso the patient can start over. - Complete.
flow.outcome.result.responseIdidentifies the submission. Ifresult.callbackUrlis set, Apex expects you to send the patient there next.
Disable Next while flow.submitting is true, and render a waiting state instead of the form while flow.outcome.kind is submitting, to avoid double submissions. Render validationError inside an element with role="alert".
Custom draft store in React
Section titled “Custom draft store in React”The engine accepts a DraftStore so drafts can live on your server instead of in localStorage, but the React hook does not pass one through yet. Build the engine yourself and mirror its state with useSyncExternalStore:
import { useEffect, useRef, useSyncExternalStore } from 'react';import { createSurveyV2Engine, type DraftStore, type SurveyV2Engine } from '@apextelemed/survey-core';
export function useSurveyEngine(draftStore: DraftStore) { const ref = useRef<SurveyV2Engine | null>(null); if (ref.current === null) { ref.current = createSurveyV2Engine({ apiBaseUrl: 'https://apextelemed.com/api', publishableKey: 'pk_0123456789abcdef0123456789abcdef', drugIds: ['drug-A'], draftStore, }); } const engine = ref.current; useEffect(() => () => engine.destroy(), [engine]); const state = useSyncExternalStore(engine.subscribe, engine.getState, engine.getState); return { engine, state };}From there, derive currentStep and the visible questions exactly as the adapter does. The adapter’s source is a good template: packages/react/src/useSurveyV2Flow.ts. Implementing a DraftStore is covered in Headless core.