UserReason

Please visit this site
on desktop.

Our mobile site is still under construction. For the full experience, open UserReason on your desktop or laptop.

Framework guides

Add in-app surveys to React

Initialize UserReason once in a browser provider, then report an application event after the relevant action succeeds. This React example offers optional feedback after a local backend confirms that cancellation is scheduled. Feedback does not decide whether cancellation goes through.

Updated · Read as Markdown

A cancellation flow with a separate feedback step

The app shows a fictional Starter subscription, a cancellation view and a confirmed scheduled state. Its local demo API accepts the cancellation before the app reports subscription_cancellation_scheduled to UserReason. The published survey can then ask for a reason.

The download is an integration example, not a billing integration. Its local backend does not cancel a real subscription, charge a card or manage customer access. Replace that demo operation with your own authenticated billing workflow before using this pattern in a product.

The React sample shows an optional cancellation survey with five reason choices after scheduling is confirmed.
The downloadable React sample with synthetic local data. The feedback survey follows a simulated confirmation; it does not change a real subscription.

Run the example and connect a test site

Use the download’s package-lock.json for its exact React, Vite and TypeScript versions. It uses the public @userreason/js 0.5.0 package and keeps the SDK disabled while the public site ID is empty or invalid.

Scroll horizontally to see all columns.

Example dependencyPinned version or requirement
UserReason browser SDK@userreason/js 0.5.0
React and React DOM19.3.1
Vite7.3.7
Vite React plugin5.1.4
TypeScript7.0.2
Node.js22.12 or newer
Public CLI used by this guide@userreason/cli 0.1.1
Guide and recipe review18 October 2026
Run the downloaded example
unzip react-cancellation.zip
cd react-cancellation
npm ci
cp .env.example .env.local
npm run dev

Scroll horizontally to see all columns.

Environment variableValue
VITE_USERREASON_SITE_IDThe public UUID of a dedicated UserReason test site.
VITE_USERREASON_API_BASE_URLOptional API origin. Leave empty for https://api.userreason.com.

Vite exposes VITE_ values to browser code. Put only the public site ID and intended API origin here; keep management credentials, billing secrets and database URLs on your server. Restart Vite after changing .env.local.

Use the same environment for your site, CLI and SDK. A site created through dash-dev.userreason.com needs https://api-dev.userreason.com and the CLI’s --dev option. If the site restricts origins, allow the exact local origin printed by the development server.

The download includes its own browser test. After installing Google Chrome, run the commands below to type-check, build and exercise the synthetic cancellation and survey paths. The README also describes the optional Chromium setup. These tests use local fixtures and do not cancel a real subscription or write customer responses.

Verify the downloaded example
npm run typecheck
npm run build
npm test

Give the SDK one owner in the React tree

The root provider in src/feedback.tsx owns the shared SDK instance. Its effect initializes UserReason, observes readiness and errors, and removes its listener and destroys the instance on cleanup. Keep that provider above the billing and cancellation views instead of initializing again in every component.

Lifecycle effect inside the sample FeedbackProvider
useEffect(() => {
  if (!configured) return;
  let active = true;
  let unsubscribe = () => {};
  try {
    userreason.init({
      siteId,
      storage: 'memory',
      automaticTriggers: false,
      ...(apiBaseUrl ? { apiBaseUrl } : {}),
    });
    unsubscribe = userreason.on(event => {
      if (active && event.type === 'error') setStatus('unavailable');
    });
    void userreason.ready().then(
      () => { if (active) setStatus('ready'); },
      () => { if (active) setStatus('unavailable'); },
    );
  } catch {
    setStatus('unavailable');
  }
  return () => {
    active = false;
    unsubscribe();
    userreason.destroy();
  };
}, []);

This excerpt belongs inside the downloaded FeedbackProvider, which supplies the public configuration and status state. Import and reuse that provider rather than mounting a second SDK initializer. The effect runs in the browser; rendering this component on a server does not initialize a visitor’s browser.

React Strict Mode can exercise an extra setup and cleanup cycle during development. Keeping initialization and teardown together lets the provider handle that cycle and ordinary view changes. Do not use reset() for navigation; it establishes a new visitor identity and belongs at sign-out, not routine view changes.

This sample uses memory storage and automaticTriggers: false. Explicit event() calls still work, while automatic page-load, selector and delay observers are disabled. Memory storage avoids persistent visitor state on the device; it still permits network requests and storage of submitted answers by UserReason. Choose production storage and consent behavior for your own application.

If your app needs signed-in survey targeting

The downloadable example stays anonymous. A signed-in application can add identity as a separate integration, using its own authentication system. This optional adaptation is not part of the sample’s browser test.

Optional initialization for signed-in targeting
userreason.init({ siteId, waitForIdentity: true });

Resolve your application’s session independently, then identify the account using a stable ID from that session. Do not await ready() before resolving authentication when waitForIdentity is true: readiness is waiting for identify(). In the following excerpt, account is the authenticated account returned by your own application.

After your app resolves the signed-in account
userreason.identify(account.id);
await userreason.ready();

When switching directly to another signed-in account, call identify() with that account’s stable ID. On sign-out, call reset() to clear the prior identity; waitForIdentity mode then waits for the next identity. Use destroy() for provider teardown, not as a substitute for sign-out. Browser identity values remain unverified unless you add a supported backend assertion.

Emit the event after the backend confirms scheduling

Opening the cancellation view shows intent. Pressing its confirmation button starts a request. src/demo-api.ts calls the local POST /api/demo/subscription/cancel endpoint and requires a successful response with demo: true, cancel_at_period_end: true and plan_tier: starter. src/App.tsx enters its Cancellation scheduled state before an effect attempts optional feedback.

Inside FeedbackProvider, after confirmed scheduling
void userreason.event('subscription_cancellation_scheduled', {
  flow_version: 'react-cancellation-v1',
  plan_tier: 'starter',
}).catch(() => setStatus('unavailable'));

Keep the success check in your application’s cancellation handler. A failed request, opening the view or leaving the page must not emit this event. Cancellation scheduling is also different from the subscription actually ending at the end of its paid period.

The event contains two fixed categories. It sends no account ID, billing identifier, email, form value or page URL. Waiting for your own backend confirmation improves the meaning of the event, but a browser-reported value still does not establish trusted billing or identity facts in UserReason.

A true event() result means a matching survey was mounted; false means none mounted. Neither result is a billing confirmation or proof that an answer was saved. Feedback failure must leave the already-confirmed cancellation intact.

The provider attempts the event once per app visit, and the app guards duplicate cancellation requests. If feedback is unavailable or still connecting when cancellation succeeds, the sample leaves cancellation scheduled and does not replay feedback automatically later. Do not make a second billing request just to try to show a survey.

Register the event, create a draft and review it

Use the CLI to create a draft from the downloaded survey.json, or recreate its questions and conditions in the dashboard. The dashboard does not offer a JSON-paste importer. The following commands use public CLI 0.1.1; replace WORKSPACE_UUID and SITE_UUID with the test site’s values. For the separate development API, add --dev to each userreason command, including login.

Install the CLI and request access
npm install -g @userreason/cli@0.1.1
userreason login --open
userreason grant request --workspace WORKSPACE_UUID --site SITE_UUID --scope site:read --scope site:update --scope survey:write --scope campaign:publish --wait

Approve the printed grant URL. Register subscription_cancellation_scheduled on the same site if it is absent, including for a registered-only event policy. Inspect and reuse an existing definition rather than creating a duplicate. The download’s event.json declares the two string properties sent by the app.

event.json
{
  "event_key": "subscription_cancellation_scheduled",
  "properties": {
    "flow_version": {
      "type": "string",
      "required": true
    },
    "plan_tier": {
      "type": "string",
      "required": true
    }
  }
}
Register the event and save the survey draft
userreason event create --workspace WORKSPACE_UUID --site SITE_UUID --file event.json --purpose business --json
userreason prompt create --workspace WORKSPACE_UUID --site SITE_UUID --file survey.json --json

The Business event preset is a collection-policy choice when retained history of this low-volume event is useful. It does not detect cancellations or supply billing truth. Review event retention independently of the survey.

Open links.edit from the create result. Check the question paths, optional follow-up, partial collection, subscription_cancellation_scheduled trigger and flow_version equal to react-cancellation-v1 event-property audience. The recipe uses 100% sampling and at most one display per tracked visitor in enrollment cancellation-v1; site-wide limits still apply.

Read the draft and current revision
userreason prompt get PROMPT_UUID --workspace WORKSPACE_UUID --site SITE_UUID --json

Replace PROMPT_UUID with the ID returned by create. Replace CURRENT_REVISION below with the revision returned by get. Saving a draft does not make it live; publish the reviewed revision explicitly.

Publish after review
userreason prompt publish PROMPT_UUID --workspace WORKSPACE_UUID --site SITE_UUID --revision CURRENT_REVISION --json

Verify cancellation and feedback independently

  1. Connect the test site, publish the recipe and wait until feedback is ready.
  2. Open the cancellation view. Confirm that opening it alone does not show a survey.
  3. Use the example’s failure case. A failed cancellation must show no scheduled state and emit no cancellation-scheduled event.
  4. Complete a successful demo cancellation. Confirm the scheduled state first, then answer the survey if it appears.
  5. Open that survey’s Responses tab in UserReason and check the saved answer and response status. The event’s boolean return is not this verification.
  6. Choose a reason with a follow-up, advance past the reason, and close before final submission. With partial collection enabled, check the Partial filter for the saved reason.

An answer being typed is not saved by itself. Partial collection saves validated answers as the visitor advances; the final submission completes the response. An unfinished survey must not undo, delay or obscure a cancellation that your backend already accepted.

The sample backend and automated fixture use synthetic local data. They cannot establish that a real billing provider cancelled a subscription or that a production response reached your workspace. Check those paths separately in your own integration.

Find the boundary that failed

Scroll horizontally to see all columns.

SymptomCheck
No SDK requestsSet a valid public VITE_USERREASON_SITE_ID and restart Vite. The example intentionally disables feedback for an empty or invalid value.
No cancellation-scheduled eventCheck that the backend request succeeded and confirmed cancel_at_period_end: true. Opening the form or receiving an error should not produce this event.
Scheduled, but no surveyCheck SDK readiness, the published site, event registration, exact trigger, flow_version audience, sampling, frequency and response allowance.
Repeat tests show no new surveyThe app and survey guard repeated attempts. Use a fresh test visit when appropriate; do not remove production frequency limits to make a test pass.
Expected partial answer missingAdvance past the answered question with partial collection enabled. An unsubmitted edit is not retained.
Feedback fails after cancellationKeep the confirmed cancellation visible and complete. Investigate feedback separately.