Skip to main content

useForma Hook

The useForma hook provides complete form state management for custom form implementations.

Basic Usage​

CustomForm.tsx
import { useForma } from "@fogpipe/forma-react";
import type { Forma } from "@fogpipe/forma-core";

function CustomForm({ spec }: { spec: Forma }) {
const forma = useForma({
spec,
onSubmit: (data) => console.log("Submitted:", data),
});

return (
<form
onSubmit={(e) => {
e.preventDefault();
forma.submitForm();
}}
>
{spec.fields &&
Object.entries(spec.fields).map(([fieldId, fieldDef]) => {
if (!forma.visibility[fieldId]) return null;

return (
<div key={fieldId}>
<label>{fieldDef.label}</label>
<input
value={String(forma.data[fieldId] || "")}
onChange={(e) => forma.setFieldValue(fieldId, e.target.value)}
onBlur={() => forma.setFieldTouched(fieldId)}
/>
</div>
);
})}

<button type="submit" disabled={!forma.isValid || forma.isSubmitting}>
Submit
</button>
</form>
);
}

Options​

const forma = useForma({
spec, // Required: Forma specification
initialData, // Optional: Initial form values
onSubmit, // Optional: Submit handler
onChange, // Optional: Change handler
validateOn, // Optional: When to validate
referenceData, // Optional: Additional reference data
validationDebounceMs, // Optional: Debounce validation
on, // Optional: Event listeners
});

Options Reference​

OptionTypeDefaultDescription
specFormarequiredThe Forma specification
initialDataRecord<string, unknown>{}Initial form values
onSubmit(data) => void-Called on valid submission
onChange(data, computed) => void-Called on any value change
validateOn"change" | "blur" | "submit""blur"When to run validation
referenceDataRecord<string, unknown>-Additional reference data
validationDebounceMsnumber0Debounce validation (ms)
onFormaEvents-Event listeners for side effects

Return Value​

The hook returns an object with state and methods:

State Properties​

PropertyTypeDescription
dataRecord<string, unknown>Current form values
computedRecord<string, unknown>Computed field values
visibilityRecord<string, boolean>Field visibility state
requiredRecord<string, boolean>Field required state
enabledRecord<string, boolean>Field enabled state
readonlyRecord<string, boolean>Field readonly state
optionsVisibilityOptionsVisibilityResultVisible options per select field, keyed by field path
errorsFieldError[]All validation errors
touchedRecord<string, boolean>Which fields have been touched
isValidbooleanWhether form is valid
isDirtybooleanWhether any field has changed
isSubmittedbooleanWhether form has been submitted
isSubmittingbooleanWhether submission is in progress
specFormaThe resolved Forma specification
wizardWizardHelpers | nullWizard navigation (if pages defined)

Methods​

MethodDescription
setFieldValue(path, value)Set a field's value
setFieldTouched(path, touched?)Mark field as touched
setValues(values)Set multiple values at once
validateField(path)Validate a single field
validateForm()Validate entire form
submitForm()Submit the form
resetForm()Reset to initial values
on(event, listener)Subscribe to lifecycle events; returns unsubscribe

Working with Field Values​

Setting Values​

// Set a single field
forma.setFieldValue("email", "user@example.com");

// Set nested field (for objects)
forma.setFieldValue("address.city", "New York");

// Set array item
forma.setFieldValue("items[0].name", "Widget");

// Set multiple values
forma.setValues({
firstName: "John",
lastName: "Doe",
email: "john@example.com",
});

Reading Values​

const email = forma.data.email;
const city = forma.data.address?.city;
const firstItem = forma.data.items?.[0];

// Matrix field values (object keyed by row ID)
const ratings = forma.data.service_rating as Record<string, number> | undefined;
const speedRating = ratings?.speed;

Validation​

Validate on Different Events​

// Validate on blur (default)
const forma = useForma({ spec, validateOn: "blur" });

// Validate on every change
const forma = useForma({ spec, validateOn: "change" });

// Validate only on submit
const forma = useForma({ spec, validateOn: "submit" });

Manual Validation​

// Validate a single field
forma.validateField("email");

// Validate entire form
const result = forma.validateForm();
console.log(result.valid); // boolean

Accessing Errors​

// All errors
forma.errors.forEach((error) => {
console.log(`${error.field}: ${error.message}`);
});

// Errors for specific field
const emailErrors = forma.errors.filter((e) => e.field === "email");

// Check if field has errors
const hasEmailError = forma.errors.some((e) => e.field === "email");

Computed Values​

Computed values are calculated automatically and available in forma.computed:

OrderTotal.tsx
function OrderTotal({ spec }: { spec: Forma }) {
const forma = useForma({ spec });

return (
<div>
<p>Subtotal: ${forma.computed.subtotal}</p>
<p>Tax: ${forma.computed.tax}</p>
<p>Total: ${forma.computed.total}</p>
</div>
);
}

Visibility and Required State​

DynamicField.tsx
function DynamicField({ fieldId, forma }) {
// Check if field should be shown
if (!forma.visibility[fieldId]) {
return null;
}

return (
<div>
<label>
{fieldDef.label}
{forma.required[fieldId] && <span>*</span>}
</label>
<input
disabled={!forma.enabled[fieldId]}
// ...
/>
</div>
);
}

Form Submission​

SubmitButton.tsx
function SubmitButton({ forma }) {
const handleSubmit = async () => {
// Validate before submitting
const result = forma.validateForm();
if (!result.valid) {
return;
}

// Submit will call onSubmit callback
await forma.submitForm();
};

return (
<button
onClick={handleSubmit}
disabled={!forma.isValid || forma.isSubmitting}
>
{forma.isSubmitting ? "Submitting..." : "Submit"}
</button>
);
}

Change Tracking​

UnsavedChangesPrompt.tsx
function UnsavedChangesPrompt({ forma }) {
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (forma.isDirty) {
e.preventDefault();
e.returnValue = "";
}
};

window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [forma.isDirty]);

return null;
}

Reset Form​

function FormActions({ forma }) {
return (
<div>
<button onClick={() => forma.resetForm()}>Reset</button>
<button onClick={() => forma.submitForm()}>Submit</button>
</div>
);
}

Array Fields​

For fields with type: "array", use getArrayHelpers() to get manipulation functions.

Getting Array Helpers​

const { getArrayHelpers } = useForma({ spec });

// Get helpers for a specific array field
const medicationsHelpers = getArrayHelpers("medications");

ArrayHelpers Properties​

PropertyTypeDescription
itemsunknown[]Current array items
minItemsnumberMinimum items allowed
maxItemsnumberMaximum items allowed
canAddbooleanWhether more items can be added
canRemovebooleanWhether items can be removed

ArrayHelpers Methods​

MethodDescription
push(item?)Add item to end of array
insert(index, item)Insert item at specific index
remove(index)Remove item at index
move(from, to)Move item from one index to another
swap(indexA, indexB)Swap items at two indices
getItemFieldProps(index, fieldName)Get props for an item's field

Basic Array Operations​

MedicationsList.tsx
function MedicationsList({ forma }) {
const helpers = forma.getArrayHelpers("medications");

return (
<div>
{helpers.items.map((item, index) => (
<div key={index}>
<input
value={item.name || ""}
onChange={(e) =>
forma.setFieldValue(`medications[${index}].name`, e.target.value)
}
/>
<button
onClick={() => helpers.remove(index)}
disabled={!helpers.canRemove}
>
Remove
</button>
</div>
))}

<button
onClick={() => helpers.push({ name: "", dosage: "" })}
disabled={!helpers.canAdd}
>
Add Medication
</button>
</div>
);
}

Using getItemFieldProps​

For cleaner code, use getItemFieldProps to get props for item fields:

ArrayItem.tsx
function ArrayItem({ helpers, index }) {
const nameProps = helpers.getItemFieldProps(index, "name");
const quantityProps = helpers.getItemFieldProps(index, "quantity");

return (
<div>
<input
value={nameProps.value || ""}
onChange={(e) => nameProps.onChange(e.target.value)}
onBlur={nameProps.onBlur}
/>
<input
type="number"
value={quantityProps.value || ""}
onChange={(e) => quantityProps.onChange(Number(e.target.value))}
onBlur={quantityProps.onBlur}
/>
</div>
);
}

Respecting Min/Max Items​

ArrayField.tsx
function ArrayField({ forma, fieldId }) {
const helpers = forma.getArrayHelpers(fieldId);

return (
<div>
{helpers.items.map((_, index) => (
<div key={index}>
{/* ... item fields ... */}
<button
onClick={() => helpers.remove(index)}
disabled={!helpers.canRemove}
title={
!helpers.canRemove
? `Minimum ${helpers.minItems} items required`
: undefined
}
>
Remove
</button>
</div>
))}

<button
onClick={() => helpers.push()}
disabled={!helpers.canAdd}
title={
!helpers.canAdd
? `Maximum ${helpers.maxItems} items allowed`
: undefined
}
>
Add Item ({helpers.items.length}/{helpers.maxItems || "∞"})
</button>
</div>
);
}