Server-rendered

Overview

Choose server-rendered forms when you want the simplest React integration and you are happy for Formie to keep owning the form UI.

If you want to see server-rendered forms in a fuller app setup, use the React starter as a working example.

In this setup:

  • React owns the host component
  • Formie still owns the rendered HTML inside it
  • the browser package still owns validation, submit flow, and browser-side behavior

Browser events and browser modules still apply for server-rendered forms. Use the Browser docs when you need deeper browser behavior extension points.

Component

This is the simplest HTML-mode entry point.

tsx
import { FormieForm } from '@verbb/formie-react';
import '@verbb/formie-browser/css/formie.css';

export function ContactForm() {
  return (
    <FormieForm
      transport="rest"
      endpoint="https://formie.test"
      formHandle="contactForm"
      theme="formie"
      onSuccess={(result) => {
        console.log('Submit ok:', result);
      }}
      onError={(result) => {
        console.log('Submit failed:', result);
      }}
    />
  );
}

Props

<FormieForm /> supports these HTML-mode props:

NameTypeRequiredDescription
transport'rest' | 'graphql'Yes, unless source=Chooses how the HTML payload is loaded.
endpointstringYes, unless source=For REST, pass the Craft base URL and Formie resolves the browser action URLs. For GraphQL, pass the GraphQL endpoint directly, usually /api.
formHandlestringYes, unless source=Handle of the form to load.
refreshTokensbooleanNoRefreshes CSRF, request, render, and captcha tokens as Formie needs them.
localestringNoAdvanced override for locale-sensitive rendering and formatting. Most setups can rely on siteId.
siteIdnumberNoRequests the form for a specific site.
autoVisiblebooleanNoReveals the mounted root automatically when it becomes ready.
theme'formie' | 'none'NoUses the shipped browser theme or skips it.
themeConfigRecord<string, unknown>NoPasses additional theme configuration to the browser theme layer.
source{ payload: FormEndpointPayload }NoUses a preloaded HTML payload instead of loading one during mount.
classNamestringNoAdds a class to the host element that React renders.

With transport="graphql", GraphQL only loads the HTML payload. Submit still uses the rendered form action rather than a GraphQL mutation.

Callback props

For the common path, start with onReady, onSuccess, and onError.

<FormieForm /> supports these callback props:

NameTypeDescription
onMountfunctionCalled after the HTML form instance mounts. Receives a FormieFormInstance.
onReadyfunctionCalled when the mounted instance is ready for use. Receives a FormieFormInstance.
onUnmountfunctionCalled after the form is unmounted.
onResultfunctionCalled for every submit result. Receives a FormSubmitResult.
onSuccessfunctionCalled when submit succeeds. Receives a FormSubmitResult.
onErrorfunctionCalled when submit fails. Receives a FormSubmitResult.
onSubmitResultfunctionCalled for every submit result. Receives a FormSubmitResult.
onSubmitSuccessfunctionCalled when submit succeeds. Receives a FormSubmitResult.
onSubmitErrorfunctionCalled when submit fails. Receives a FormSubmitResult.
onEventfunctionCalled for browser events exposed through the React wrapper. Receives a FormieReactEvent.

The onSubmit* names remain available as the more explicit lower-level aliases.

tsx
import { FormieForm } from '@verbb/formie-react';

export function ContactForm() {
  return (
    <FormieForm
      transport="rest"
      endpoint="https://formie.test"
      formHandle="contactForm"
      theme="formie"
      onReady={(instance) => {
        console.log('Mounted:', instance.id);
      }}
      onSuccess={(result) => {
        console.log('Submit ok:', result);
      }}
      onEvent={(event) => {
        console.log(event.name, event.payload);
      }}
    />
  );
}

Advanced hook

useFormieHtml() mounts the same server-rendered browser behavior, but lets your component own the host ref and imperative API directly.

tsx
import { useFormieHtml } from '@verbb/formie-react';
import '@verbb/formie-browser/css/formie.css';

export function ContactForm() {
  const form = useFormieHtml({
    transport: 'rest',
    endpoint: 'https://formie.test',
    formHandle: 'contactForm',
    theme: 'formie',
  });

  return <div ref={form.rootRef} />;
}

This is still the server-rendered path, but it is the lower-level escape hatch rather than the recommended starting point.

Options

useFormieHtml() accepts the same HTML mount options as the browser client, except mode is fixed to 'server-rendered':

NameTypeRequiredDescription
transport'rest' | 'graphql'Yes, unless payloadChooses how the HTML payload is loaded.
endpointstringYes, unless payloadFor REST, pass the Craft base URL and Formie resolves the browser action URLs. For GraphQL, pass the GraphQL endpoint directly, usually /api.
formHandlestringYes, unless payloadHandle of the form to load.
payloadFormEndpointPayloadNoUses a preloaded HTML payload instead of loading one during mount.
refreshTokensbooleanNoRefreshes CSRF, request, render, and captcha tokens as Formie needs them.
localestringNoAdvanced override for locale-sensitive rendering and formatting. Most setups can rely on siteId.
siteIdnumberNoRequests the form for a specific site.
autoVisiblebooleanNoReveals the mounted root automatically when it becomes ready.
theme'formie' | 'none'NoUses the shipped browser theme or skips it.
themeConfigRecord<string, unknown>NoPasses additional theme configuration to the browser theme layer.

Return value

The hook returns:

NameTypeDescription
rootRefRefObject<HTMLDivElement | null>Host element ref that Formie mounts into.
state.instanceFormieFormInstance | nullMounted instance, or null before mount.
state.isMountedbooleanWhether the form is currently mounted.
state.errorError | nullMount error, if mount failed.
submitfunctionImperative submit access for the mounted form. Accepts an optional FormAction and resolves to FormSubmitResult | null.

Events with the hook

The hook does not expose callback props. Subscribe to browser events from state.instance instead:

tsx
import { useEffect } from 'react';
import { useFormieHtml } from '@verbb/formie-react';

export function ContactForm() {
  const form = useFormieHtml({
    transport: 'rest',
    endpoint: 'https://formie.test',
    formHandle: 'contactForm',
    theme: 'formie',
  });

  useEffect(() => {
    const instance = form.state.instance;

    if (!instance) {
      return;
    }

    return instance.on('formie:submit:result', (result) => {
      console.log('Submit result:', result);
    });
  }, [form.state.instance]);

  return <div ref={form.rootRef} />;
}

Preloaded payloads

If your app already fetched the HTML payload, pass it into React instead of fetching it again during mount:

tsx
import { useEffect, useState } from 'react';
import { FormieForm, type FormEndpointPayload } from '@verbb/formie-react';

async function loadFormPayload(endpoint: string, handle: string): Promise<FormEndpointPayload> {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      handle,
    }),
  });

  if (!response.ok) {
    throw new Error('Failed to load form payload.');
  }

  return response.json();
}

export function ContactScreen() {
  const [payload, setPayload] = useState<FormEndpointPayload | null>(null);

  useEffect(() => {
    void loadFormPayload('https://formie.test/your-render-endpoint', 'contactForm').then(setPayload);
  }, []);

  if (!payload) {
    return null;
  }

  return <FormieForm source={{ payload }} theme="formie" />;
}

For hook usage, pass the same payload as payload instead of source=.

Last updated: May 6, 2026, 3:46 PM