Skip to content

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-sdk

Mount 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

  • theme and primaryColor set 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);
}