FormEngineRenderer
The main component. It creates the engine, renders fields, handles navigation, validation, drafts and submission.
Basic usage
import { FormEngineRenderer } from '@squaredr/fieldcraft-react'
import schema from './contact-form.json'
export default function ContactPage() {
return (
<FormEngineRenderer
schema={schema}
onSubmit={async (response) => {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(response),
})
}}
/>
)
}Props
Required
| Prop | Type | Description |
|---|---|---|
schema | FormEngineSchema | The form schema. Validated at mount time — invalid schemas throw FormEngineSchemaError. |
Submission
| Prop | Type | Description |
|---|---|---|
onSubmit | (response: FormResponse) => void | Promise<void> | Called when the form is submitted successfully. Receives the full FormResponse with values, metadata, scores. |
adapters | SubmitAdapter | SubmitAdapter[] | One or more submission adapters (HTTP, Supabase, Postgres, webhook). Run in parallel on submit. |
Theming
| Prop | Type | Description |
|---|---|---|
theme | FormEngineTheme | Theme object or preset. Controls colours, typography, spacing, and shape. |
className | string | CSS class added to the root form element. |
Field registry
| Prop | Type | Description |
|---|---|---|
components | FieldRegistry | Custom field component map. Merged with the default registry — your components override built-in ones for matching types. |
Data
| Prop | Type | Description |
|---|---|---|
prefill | Record<string, unknown> | Initial values to prefill into the form. Keys are field IDs. |
initialValues | Record<string, unknown> | Same as prefill — alternative prop name. |
Validators
| Prop | Type | Description |
|---|---|---|
validators | Record<string, CustomValidator> | Custom sync validators keyed by name. Referenced in schema via { type: 'custom', name: '...' }. |
asyncValidators | Record<string, AsyncValidator> | Custom async validators keyed by name. Referenced in schema via { type: 'async', endpoint: '...' }. |
Drafts
| Prop | Type | Description |
|---|---|---|
sessionToken | string | Custom session token. If not provided, a UUID is generated. Used to scope drafts per user/session. |
draftAdapter | DraftAdapter | Server-side draft adapter (Supabase, Postgres, or custom). Enables cross-device draft persistence. |
autoSaveIntervalMs | number | Auto-save interval in milliseconds. When set, the engine saves drafts automatically at this interval. |
draftMigrations | Record<string, (draft: DraftSnapshot) => DraftSnapshot> | Migration functions keyed by schema version. Applied when loading a draft saved against an older schema version. |
Labels
| Prop | Type | Description |
|---|---|---|
prevLabel | string | Label for the "Back" button. Default: "Back". |
nextLabel | string | Label for the "Next" button. Default: "Next". |
submitLabel | string | Label for the "Submit" button. Default: "Submit". |
Callbacks
| Prop | Type | Description |
|---|---|---|
onSectionChange | (sectionId: string, index: number) => void | Called when the active section changes. |
onFieldChange | (fieldId: string, value: unknown) => void | Called when any field value changes. |
onReady | (engine: FormEngine) => void | Called after the engine is initialised. Receives the engine instance — use it to call engine.loadDraft(), engine.submit(), or read state programmatically. |
onValidationError | (errors: Record<string, string[]>) => void | Called when validation fails (on section change or submit). |
onStateChange | (state: FormState) => void | Called on every state change. Use sparingly — this fires frequently. |
onEvent | (event: FieldCraftEvent) => void | Called on every engine event (field interaction, section change, submit, etc.). Useful for custom logging. |
beforeSubmit | (response: FormResponse) => FormResponse | false | Promise<FormResponse | false> | Intercept submissions before they reach adapters. Return a modified response, or false to cancel the submission. |
Analytics
| Prop | Type | Description |
|---|---|---|
analytics | AnalyticsAdapter | Analytics adapter for tracking form views, starts, field interactions, submissions, and abandonment. |
metadata | Record<string, unknown> | Arbitrary metadata attached to form responses and analytics events. Useful for tracking source, campaign, or user context. |
Behaviour
| Prop | Type | Description |
|---|---|---|
autoFocus | boolean | When true, the first field in each section receives focus automatically on mount and section change. |
autoAdvance | boolean | Conversational mode only. When true, the form advances to the next question automatically after the user answers. |
hideNavigation | boolean | When true, built-in navigation buttons (Back, Next, Submit) are hidden across all display modes. Use onReady to get the engine instance, then call engine.nextSection(), engine.prevSection(), engine.submit() from your own UI. Pair with onStateChange to read canGoNext, canGoPrev, isSubmitting, and other state. |
Full example
import {
FormEngineRenderer,
defaultRegistry,
} from '@squaredr/fieldcraft-react'
import { createSupabaseAdapter } from '@squaredr/fieldcraft-adapters'
import { PainScaleField } from './custom-fields/PainScaleField'
import { validators } from '@/lib/validators'
import schema from './patient-intake.json'
import { supabase } from '@/lib/supabase'
import type { FormEngine } from '@squaredr/fieldcraft-core'
const adapter = createSupabaseAdapter({
client: supabase,
table: 'intake_submissions',
})
export default function IntakePage() {
return (
<FormEngineRenderer
schema={schema}
adapters={adapter}
components={{ ...defaultRegistry, pain_scale: PainScaleField }}
validators={validators}
prefill={{ referral_source: 'website' }}
onSubmit={async (response) => {
console.log('Submitted:', response.schemaId, response.values)
}}
onReady={(engine: FormEngine) => {
console.log('Form ready:', engine.getSchema().title)
}}
onSectionChange={(id, idx) => {
console.log(`Section ${idx + 1}: ${id}`)
}}
submitLabel="Submit Intake Form"
autoFocus
/>
)
}Headless / custom navigation
Use hideNavigation with onReady and onStateChange to replace the built-in buttons with your own UI:
import { useState, useCallback } from 'react'
import { FormEngineRenderer } from '@squaredr/fieldcraft-react'
import type { FormEngine, FormState } from '@squaredr/fieldcraft-core'
export default function CustomNavForm({ schema }) {
const [engine, setEngine] = useState<FormEngine | null>(null)
const [formState, setFormState] = useState<FormState | null>(null)
return (
<div>
<FormEngineRenderer
schema={schema}
hideNavigation
onReady={setEngine}
onStateChange={setFormState}
onSubmit={(response) => console.log(response)}
/>
{engine && formState && (
<div className="flex gap-2 mt-4">
<button
onClick={() => engine.prevSection()}
disabled={!formState.canGoPrev}
>
Back
</button>
{formState.currentSectionIndex < formState.totalVisibleSections - 1 ? (
<button
onClick={() => engine.nextSection()}
disabled={!formState.canGoNext}
>
Continue
</button>
) : (
<button
onClick={() => engine.submit()}
disabled={formState.isSubmitting}
>
{formState.isSubmitting ? 'Sending…' : 'Send'}
</button>
)}
</div>
)}
</div>
)
}How it works internally
FormEngineRenderer is a wrapper that:
- Creates a
FormEngineinstance viauseFormEngine(schema, options) - Subscribes to state changes via
useSyncExternalStore - Selects the rendering strategy based on
schema.settings.displayMode:- stepped (default) — one section at a time with Back/Next/Submit buttons and a progress bar
- classic — all visible sections rendered at once with a Submit button at the bottom
- conversational — one question at a time with Enter key support and question-level progress
- Handles navigation, validation, and submission
- Manages draft persistence if
settings.allowDraftSaveistrue
See Display modes for details on each mode.
The engine lives in a useRef and is created once. React Strict Mode double-mounts don't create multiple engines.
Using the engine directly
If FormEngineRenderer doesn't fit your layout, use useFormEngine to get the engine and build your own UI:
import { useFormEngine } from '@squaredr/fieldcraft-react'
function CustomForm({ schema }) {
const engine = useFormEngine(schema, {
onSubmit: async (response) => { /* ... */ },
})
return (
<div>
<h1>{engine.getSchema().title}</h1>
<p>Progress: {engine.state.progressPercent}%</p>
{/* Render fields manually */}
</div>
)
}See Hooks for the full hook API.
Next steps
- Hooks — useFormEngine, useFieldValue, useFieldError, useSectionProgress
- Theming — customise the visual appearance
- Custom field types — register your own field components