Connecting to Your Backend
FormRenderer is completely headless regarding data — you inject your own functions for file uploads, postal code (CEP) lookup, terms URL resolution, and dynamic field props.
uploadFile
Called when the user selects a file in a FILE field.
<FormRenderer
schema={schema}
uploadFile={async (file, fieldName, onProgress) => {
const form = new FormData();
form.append("file", file);
form.append("field", fieldName);
const res = await fetch("/api/upload", {
method: "POST",
body: form,
headers: { Authorization: `Bearer ${token}` },
});
const { id } = await res.json();
return id; // must return a string (URL or upload ID)
}}
onComplete={handler}
/>
The onProgress parameter receives a percentage (0–100) if the endpoint supports it:
uploadFile={async (file, fieldName, onProgress) => {
return await uploadWithProgress(file, onProgress);
}}
cepLookup
Called by CEP type fields for auto-fill. By default, the renderer uses the public ViaCEP API.
<FormRenderer
schema={schema}
cepLookup={async (cep, signal) => {
const res = await fetch(`/api/cep/${cep}`, { signal });
const data = await res.json();
return {
logradouro: data.street,
bairro: data.neighborhood,
cidade: data.city,
estado: data.state,
};
}}
onComplete={handler}
/>
CepLookupResult
interface CepLookupResult {
logradouro?: string;
bairro?: string;
cidade?: string;
estado?: string;
erro?: boolean; // true if the ZIP code was not found
}
The renderer uses the field's cepFillMap to map result keys to the names of other fields that should be auto-filled.
resolveTermsUploadUrl
TERMS type fields can have a linked PDF via termoPdfUploadId. This function resolves the uploadId to the PDF access URL.
<FormRenderer
schema={schema}
resolveTermsUploadUrl={async (uploadId) => {
const res = await fetch(`/api/docs/${uploadId}/url`);
const { url } = await res.json();
return url;
}}
onComplete={handler}
/>
fieldResolvers
Dynamic field props resolved at render time based on form values or externalData.
Configure on the field
In the schema, set resolvePropsKey on the field:
{
"nome": "shirt_size",
"tipo": "select",
"label": "Shirt size",
"obrigatorio": true,
"tamanho": 6,
"ordem": 3,
"resolvePropsKey": "shirt_size_options"
}
Pass the resolver
<FormRenderer
schema={schema}
externalData={{
"evento.tamanhos": ["P", "M", "G", "GG", "XGG"],
}}
fieldResolvers={{
shirt_size_options: (_field, _formValues, external) => ({
opcoes:
(external["evento.tamanhos"] as string[])?.map((s) => ({
valor: s,
label: s,
})) ?? [],
}),
// Options that depend on another form field
city_by_state: (_field, formValues) => {
const state = formValues["state"] as string;
return {
opcoes: getCitiesByState(state).map((c) => ({
valor: c,
label: c,
})),
};
},
// Field that can be conditionally disabled
locked_field: (_field, formValues) => ({
isDisabled: formValues["type"] !== "advanced",
}),
}}
onComplete={handler}
/>
Resolver type
type FieldResolver = (
field: FormField,
formValues: Record<string, unknown>,
externalData: Record<string, unknown>,
) => Partial<FormField>;
Any FormField property can be returned and it overrides the value in the schema.
externalData
External data available for:
- Variable interpolation (
{{evento.nome}}) - Conditionals with the
evento.prefix opcoesFromVarin selection fieldsfieldResolvers
<FormRenderer
schema={schema}
externalData={{
"evento.nome": "Summer Camp 2026",
"evento.valor": 5000,
"evento.dataInicio": "2026-01-15",
"evento.id": "evt-abc123",
"evento.tamanhosCamiseta": ["S", "M", "L"],
}}
onComplete={handler}
/>
In the schema, use {{evento.nome}} in any label, titulo, hint, placeholder, or termoTexto.
fieldErrors (server errors)
Validation errors returned by the backend after submission:
const [serverErrors, setServerErrors] = useState<Record<string, string>>({});
<FormRenderer
schema={schema}
fieldErrors={serverErrors}
onComplete={async (values) => {
try {
await api.submit(values);
} catch (err) {
if (err.status === 422) {
// err.errors = { email: 'Email already registered', cpf: 'CPF invalid' }
setServerErrors(err.errors);
}
}
}}
/>;