Client-Rendered

Overview ​

Choose client-rendered forms when React should render the visible form UI and you want full control over components, layout, and hooks.

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

In this setup:

  • React renders from Formie's client definition
  • Formie owns state, page progression, validation, and submission behind it
  • your app owns the visible UI through React components, field components, slots, and hooks

Component ​

Start with <FormieClientForm />:

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

export function ContactForm() {
  return (
    <FormieClientForm
      
      transport="rest"
      endpoint="https://formie.test"
      formHandle="contactForm"
    />
  );
}

This is the simplest client-rendered entry point.

Props ​

<FormieClientForm /> supports these client-rendered props:

NameTypeRequiredDescription
transport'rest' | 'graphql'Yes, unless sourceChooses how the client definition envelope is loaded.
endpointstringYes, unless sourceFor REST, pass the Craft base URL and Formie resolves the browser action URLs. For GraphQL, pass the GraphQL endpoint directly, usually /api.
formHandlestringYes, unless sourceHandle of the form to load.
siteIdnumberNoRequests the form for a specific site.
sourceFormieDefinitionSourceNoUses a preloaded client definition envelope and transport metadata.
componentsFormieReactComponentsNoReplaces top-level Form, Page, Field, or error-summary components.
fieldComponentsmapNoReplaces specific field-type renderers. Keys are ClientFieldType values and values are React components.
slotsmapNoIntercepts smaller layout regions inside the default component tree. Keys are slot names and values are React components.
classNamestringNoAdds a class to the rendered form root.

Callback Props ​

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

<FormieClientForm /> also supports these callback props:

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

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

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

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

Transport ​

Once you choose client-rendered forms, you also need to choose how the client definition envelope is loaded and how submissions are sent.

Use REST when:

  • you want the simplest transport story
  • you want the closest fit to the client-rendered controllers
  • you are wiring the app against Formie's standard public actions

Use GraphQL when:

  • your app already standardises on GraphQL
  • you want transport to stay inside an existing GraphQL client workflow
  • you want to load formieClientForm yourself or through your existing data layer

For client-rendered forms, GraphQL covers more than the initial load. Formie also uses GraphQL mutations for submit, session refresh, and page changes.

GraphQL Query ​

For GraphQL client-rendered forms, load formieClientForm:

graphql
query ClientForm($handle: String!, $siteId: Int) {
  formieClientForm(handle: $handle, siteId: $siteId) {
    contractVersion
    definition
    session {
      id
      currentPageId
      tokens
      continuation
    }
  }
}

That query returns the ClientFormBootstrap React needs: contractVersion, definition, and session.

Manual GraphQL Mutations ​

If you use <FormieClientForm transport="graphql" />, Formie handles submit, session refresh, and page changes for you.

If your app owns the GraphQL client directly, use these mutations:

  • submitFormieClientForm
  • refreshFormieClientSession
  • setFormieClientPage

For example, submit uses submitFormieClientForm:

graphql
mutation SubmitForm($input: FormieClientSubmitInput!) {
  submitFormieClientForm(input: $input) {
    success
    submissionUid
    currentPageId
    nextPageId
    previousPageId
    isFinalPage
    errors
    messages
    session {
      id
      currentPageId
      tokens
      continuation
    }
  }
}

Preloaded Definition ​

If your app already fetched the client definition envelope, pass it into source instead of loading it again inside the form component:

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

const query = `
query ClientForm($handle: String!, $siteId: Int) {
  formieClientForm(handle: $handle, siteId: $siteId) {
    contractVersion
    definition
    session {
      id
      currentPageId
      tokens
      continuation
    }
  }
}`;

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

  if (!response.ok) {
    throw new Error('Failed to load client definition envelope.');
  }

  const body = await response.json();

  return body.data.formieClientForm;
}

export function ContactScreen() {
  const [envelope, setEnvelope] = useState<ClientFormBootstrap | null>(null);

  useEffect(() => {
    void loadClientEnvelope('https://formie.test/api', 'contactForm').then(setEnvelope);
  }, []);

  if (!envelope) {
    return null;
  }

  return (
    <FormieClientForm
      
      source={{
        definition: envelope,
        transport: {
          type: 'graphql',
          endpoint: 'https://formie.test/api',
          formHandle: 'contactForm',
        },
      }}
    />
  );
}

Completion and query prefill ​

A successful final submission returns completion with behavior (message, redirect, reload or reset), url, target, message and hideForm. The standard adapter applies it. Page navigation and save-for-later results have no completion action. Payment continuation remains a separate result. Custom renderers should use this result rather than infer completion from an absent next page.

Use the REST or GraphQL loader’s query option to pass selected URL parameters for prefilling fields or forwarding campaign values. Formie captures them when the form loads; changing them in a later submission does not replace the originals. This option supplies field input, not form settings or credentials.

Last updated: Sep 27, 2026, 4:31 PM