Skip to main content

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
  • opcoesFromVar in selection fields
  • fieldResolvers
<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);
}
}
}}
/>;