BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 13, 2026ReactForm Builder

Embed a Public Form in React with BTST FormRenderer

Render an existing form inside your own page, connect the shared providers, and handle loading, submission errors, success messages, and redirects.

Embed a Public Form in React with BTST FormRenderer

You have designed a form and saved its schema. The next job is to place it inside your contact, onboarding, or feedback page without exposing the form editor to visitors. The public renderer needs the saved form's slug and the application's existing BTST runtime.

BTST 3.0.2 exports FormRenderer for this job. It fetches a saved form, renders its fields, submits answers, and displays the configured success state. This guide focuses on embedding and visitor behavior; the server submission validation guide covers the separate write boundary.

Reuse the application providers#

Place the renderer below the existing React Query and BTST providers, with the Form Builder client plugin registered in the resolved stack. A plain React component import does not configure its API base, permissions, or overrides.

Keep the page route and the form slug separate. A page at /contact can render a form whose slug is customer-enquiry; the page does not need to use the editor's route. Follow the Form Builder setup for the shared backend, client plugin, and provider wiring.

This component receives an ordinary string prop, so its framework route can resolve parameters before rendering it:

TSX
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
"use client";

import { useState } from "react";
import { FormRenderer } from "@btst/stack/plugins/form-builder/client/components";

export function PublicEnquiryForm({ slug }: { slug: string }) {
  const [submissionFailed, setSubmissionFailed] = useState(false);

  return (
    <section aria-label="Enquiry form">
      {submissionFailed && (
        <p role="alert">
          We could not send your response. Check your answers and try again.
        </p>
      )}
      <FormRenderer
        slug={slug}
        submitButtonText="Send enquiry"
        successMessage="Thanks. Your enquiry has been received."
        onError={() => setSubmissionFailed(true)}
        onSuccess={() => setSubmissionFailed(false)}
      />
    </section>
  );
}

The snippet assumes the surrounding providers and an active saved form. It does not create the form or configure email delivery. In an app where the slug changes without unmounting this component, render it with key={slug} from the parent so a previous form's local success or error state is not reused for the next form.

Test loading and submission failures separately#

LoadingComponent and ErrorComponent customize loading and form-fetch failures. onError handles a failed submission. These are different stages: an active form may load successfully while its submission is rejected by validation, authorization, or a temporary network problem.

The example displays a general submission error without putting raw server messages or submitted answers into the page's logs. For field-specific validation, inspect the renderer's existing behavior with your actual schema before adding a second set of messages.

The released renderer checks that the saved form has status active; it displays an error for inactive forms. It also checks the rendered form and submission permissions. Those browser checks do not replace the backend rules. Public form reading and answer submission should be explicitly allowed only as intended; form editing and viewing stored submissions can stay restricted.

Decide the success destination#

The renderer can use a success message from the saved form or a successMessage prop override. If the submission response supplies a redirect URL, the released component invokes onSuccess and then navigates to that URL.

Choose whether this form should show an inline confirmation or leave the page. If the saved configuration includes a redirect, a custom success message does not cancel that redirect. Verify the destination in the editor and application policy, and test the live navigation using non-sensitive test answers.

Use onSuccess for optional UI or a privacy-preserving completion event. Avoid throwing from that callback: in this release it runs inside the submission handler's try block, so a callback error can be presented as a failure after the server has already accepted the answer. A browser callback is also not a durable delivery guarantee for email or downstream processing.

Keep custom fields compatible#

fieldComponents lets you provide custom field renderers. The public form and editor need to agree on the field metadata those components understand. Test labels, keyboard navigation, error descriptions, and all required input types, including any multi-step structure.

Before release, test an active form, an inactive form, a missing slug, a rejected submission, a successful retry, and the configured success destination. Confirm that anonymous visitors can submit only what your server policy permits and cannot read submissions or edit forms. Then use the Form Builder reference for the complete props, permission configuration, and customization options.

In This Post

Reuse the application providersTest loading and submission failures separatelyDecide the success destinationKeep custom fields compatible