Surveys
Show NPS, CSAT and custom surveys inside your React app. Build them in the dashboard with branching and several endings, then mount RefletSurveys once.
Setup
npm install reflet-sdkMount RefletSurveys once inside RefletProvider. It loads the surveys this visitor is eligible for, arms their triggers and shows one survey at a time in a floating card. The card lives in its own shadow root, so your CSS never restyles it.
import { RefletProvider, RefletSurveys } from "reflet-sdk/react";
export function App({ children, userToken }) {
return (
<RefletProvider publicKey="fb_pub_xxx" userToken={userToken}>
{children}
<RefletSurveys />
</RefletProvider>
);
}Pass a signed userToken (see user signing) or user to credit responses to your users and recognize them across devices. Anonymous visitors get a stable ID in localStorage, so “show once” holds across visits. Not using React? The feedback widget delivers the same surveys with a script tag.
Triggers
Each survey has one trigger, picked in the dashboard. A survey shows at most once per page load, and never while another survey is open: a trigger that fires meanwhile stays armed.
Who sees a survey
The API decides who may see a survey before any trigger fires, so a visitor outside the audience never sees it, whichever way it’s triggered. A survey counts as seen once its first screen appears.
useRefletSurveys
Drive RefletSurveys from any component inside the provider: send custom events, open a manual survey from a button, or close the survey on screen. Calls made before surveys have loaded are queued and replayed.
import { useEffect } from "react";
import { useRefletSurveys } from "reflet-sdk/react";
function OrderConfirmation() {
const { track } = useRefletSurveys();
useEffect(() => {
track("checkout_completed");
}, [track]);
return <p>Thanks for your order!</p>;
}
function HelpMenu() {
const { showSurvey } = useRefletSurveys();
return (
<button onClick={() => showSurvey("kv91…")} type="button">
Tell us how we’re doing
</button>
);
}Callbacks
Forward survey activity to your analytics. Answers arrive one at a time as the respondent moves on. endingId is the ending their answers led to. onSurveyDismiss fires only when someone closes a survey before finishing it.
<RefletSurveys
onSurveyStart={({ surveyId, responseId }) => {}}
onSurveyAnswer={({ surveyId, questionId, value }) => {}}
onSurveyComplete={({ surveyId, responseId, endingId }) => {}}
onSurveyDismiss={({ surveyId, responseId, answeredCount }) => {}}
/>Theming
themeandprimaryColorset the look. With a hex color, button text switches between dark and white to stay readable.- The position (bottom right, bottom left or centered) is set per survey in the dashboard.
- An ending without a button closes itself after a few seconds.
- Respondents can use the keyboard throughout: Enter moves on, Escape closes, number keys answer NPS, rating and single-choice questions, and Y or N answers yes/no questions.
Custom survey UI
Embed the survey card
useSurveySession runs a survey’s flow: branching, validation and saving answers. Pass the session to SurveyCard to render it anywhere, for example inline on a settings page. Inside RefletProvider, answers are saved to Reflet. The response starts on mount; pass autoStart: false to start it yourself.
import { useEffect, useState } from "react";
import {
type PublicSurvey,
SurveyCard,
useRefletClient,
useSurveySession,
} from "reflet-sdk/react";
function InlineSurvey({ survey }: { survey: PublicSurvey }) {
const session = useSurveySession(survey);
return <SurveyCard session={session} variant="inline" />;
}
export function SettingsSurvey({ surveyId }: { surveyId: string }) {
const client = useRefletClient();
const [survey, setSurvey] = useState<PublicSurvey>();
useEffect(() => {
client.getEligibleSurveys().then((surveys) => {
setSurvey(surveys.find((candidate) => candidate._id === surveyId));
});
}, [client, surveyId]);
return survey ? <InlineSurvey survey={survey} /> : null;
}Build your own UI
Render the session yourself for complete control. This example handles text questions only. A new session begins whenever survey or transport changes identity, so keep them stable.
import { type PublicSurvey, useSurveySession } from "reflet-sdk/react";
function TextOnlySurvey({ survey }: { survey: PublicSurvey }) {
const { back, next, setAnswer, snapshot } = useSurveySession(survey);
const { answers, canGoBack, ending, error, isLastStep, phase, question } =
snapshot;
if (phase === "ending") {
return <p>{ending?.title}</p>;
}
if (phase !== "question" || !question) {
return null;
}
const answer = answers.get(question._id);
return (
<form
onSubmit={(event) => {
event.preventDefault();
next();
}}
>
<label htmlFor="survey-answer">{question.title}</label>
<input
id="survey-answer"
onChange={(event) => setAnswer(event.target.value || undefined)}
value={typeof answer === "string" ? answer : ""}
/>
{error && <p role="alert">{error}</p>}
{canGoBack && (
<button onClick={back} type="button">
Back
</button>
)}
<button disabled={snapshot.isSubmitting} type="submit">
{isLastStep ? "Submit" : "Next"}
</button>
</form>
);
}Preview without saving
reflet-sdk/surveys exports the same session hook and card without needing the provider. With previewTransport, nothing is saved, which is useful for previews and tests.
import {
previewTransport,
SurveyCard,
useSurveySession,
} from "reflet-sdk/surveys";
function SurveyPreview({ survey }) {
const session = useSurveySession(survey, { transport: previewTransport });
return <SurveyCard session={session} theme="light" variant="page" />;
}JavaScript client
The Reflet client from reflet-sdk wraps the survey endpoints and works without React. It sends the visitor’s anonymous ID, page URL and user agent for you. It doesn’t run the branching flow: walk it yourself from each question’s logic and next. Completing fails if a required question on the respondent’s path has no answer.
import { Reflet } from "reflet-sdk";
const reflet = new Reflet({ publicKey: "fb_pub_xxx", userToken });
const [survey] = await reflet.getEligibleSurveys();
if (survey) {
const { responseId } = await reflet.startSurveyResponse({
surveyId: survey._id,
});
await reflet.submitSurveyAnswer({
responseId,
questionId: survey.questions[0]._id,
value: 9,
});
const { endingId } = await reflet.completeSurveyResponse(responseId);
}