Skip to main content

FormRenderer

The main component that renders a FormSchema as an interactive multi-step form.

Import

import { FormRenderer } from "@schema-forms-data/renderer";
// or via the meta-package
import { FormRenderer } from "@schema-forms-data/react";

Props

Required

PropTypeDescription
schemaFormSchemaThe form schema. See Core Types.

Events

PropTypeDescription
onSubmitStep(stepIndex: number, data: Record<string, unknown>) => Promise<void>Called when the user advances to the next step. Use it to save drafts.
onComplete(data: Record<string, unknown>) => Promise<void>Called on final submit with all accumulated values from all steps.
onValuesChange(values: Record<string, unknown>) => voidCalled on every field change in the current step — useful for autosave.

Visual

PropTypeDefaultDescription
formTitlestringundefinedTitle displayed at the top of the form.
templatestring | null"moderno"Visual template ID. See Templates.
classNamestringundefinedExtra CSS class on the root wrapper.
StepIndicatorReact.ComponentType<StepIndicatorProps>built-inCustom step indicator component.

Data

PropTypeDefaultDescription
initialValuesRecord<string, unknown>{}Initial field values when the form mounts.
initialStepnumber0Initial step (0-based index).
externalDataRecord<string, unknown>{}Data available for variable interpolation ({{evento.nome}}) and conditionals with source: 'evento'.
fieldErrorsRecord<string, string>{}Field errors from the server (e.g. a 422 response), applied after submit.

Integrations

PropTypeDescription
uploadFile(file: File, fieldName: string, onProgress?: (pct: number) => void) => Promise<string>Called when the user selects a file. Return the upload ID or URL.
deleteUploadedFile(uploadId: string) => Promise<void>Called automatically before replacing a file, to delete the previous one from storage. Errors are silenced.
cepLookup(cep: string, signal?: AbortSignal) => Promise<CepLookupResult>ZIP/postal code lookup. Defaults to the public ViaCEP API.
resolveTermsUploadUrl(uploadId: string) => Promise<string>Resolves the preview URL of a terms PDF from an uploadId.
fieldResolversFieldResolversDynamic per-field props resolved at render time. See Injection.
validatorMapperValidatorMapperCustom (async) validation functions. See Validation.
componentMapperComponentMapperReplaces the default component for any field type. See Component Mapper.
paymentMethodOptions{ porDia?: PaymentOption[]; todosOsDias?: PaymentOption[] }Custom options for the payment_method field.

Submit error handling

If onSubmitStep or onComplete throws, FormRenderer catches the error, hides the spinner, and displays the error message below the navigation buttons. The user can correct and retry without reloading the page.

Reset the form

To reset the form programmatically, use the useFormApi() hook from a child component:

import { useFormApi } from "@schema-forms-data/renderer";

function ResetButton() {
const api = useFormApi();
return <button onClick={() => api.reset()}>Start over</button>;
}

Full example

import { FormRenderer } from "@schema-forms-data/renderer";
import { schema } from "./mySchema";

export function RegistrationForm() {
return (
<FormRenderer
schema={schema}
formTitle="Event Registration"
template="acampamento"
externalData={{
"evento.nome": "Camp 2026",
"evento.valor": 5000,
"evento.id": "abc123",
}}
initialValues={{ nome: "John Doe" }}
uploadFile={async (file, fieldName, onProgress) => {
const form = new FormData();
form.append("file", file);
form.append("campo", fieldName);
const res = await fetch("/api/upload", { method: "POST", body: form });
const { id } = await res.json();
return id;
}}
deleteUploadedFile={async (uploadId) => {
await fetch(`/api/upload/${uploadId}`, { method: "DELETE" });
}}
cepLookup={async (cep) => {
const res = await fetch(`/api/cep/${cep}`);
return res.json(); // { logradouro, bairro, cidade, estado }
}}
validatorMapper={{
cpfUnico: async (value, _all, _cfg, external) => {
const exists = await api.checkCpf(
String(value),
String(external["evento.id"]),
);
return exists ? "CPF already registered for this event" : undefined;
},
}}
fieldErrors={serverErrors} // { email: 'Invalid email' }
onValuesChange={(values) =>
sessionStorage.setItem("draft", JSON.stringify(values))
}
onSubmitStep={async (stepIndex, data) => {
await api.saveDraft(stepIndex, data);
}}
onComplete={async (values) => {
await api.finalize(values);
}}
/>
);
}

Custom StepIndicator

Replace the built-in step indicator with your own component:

import type { StepIndicatorProps } from "@schema-forms-data/renderer";

function MyIndicator({ steps, currentStep, onStepClick }: StepIndicatorProps) {
return (
<div className="indicator">
{steps.map((step, i) => (
<button
key={step.id}
className={
i === currentStep ? "active" : i < currentStep ? "done" : ""
}
onClick={() => i < currentStep && onStepClick?.(i)}
>
{step.label}
</button>
))}
</div>
);
}

<FormRenderer
schema={schema}
StepIndicator={MyIndicator}
onComplete={handler}
/>;
interface StepIndicatorProps {
steps: Array<{ id: string; label?: string; icone?: string }>;
currentStep: number;
onStepClick?: (index: number) => void;
}