Skip to main content

useForma Hook

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

Basic Usage

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];

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:

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

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

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

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

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>
);
}

Drag and Drop Reordering

Use move() for drag-and-drop functionality:

import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

function SortableList({ forma }) {
const helpers = forma.getArrayHelpers("items");

const handleDragEnd = (result) => {
if (!result.destination) return;
helpers.move(result.source.index, result.destination.index);
};

return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="items">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{helpers.items.map((item, index) => (
<Draggable key={index} draggableId={String(index)} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.name}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}

Inserting at Specific Position

function insertAtBeginning() {
helpers.insert(0, { name: "", value: "" });
}

function insertAfterCurrent(currentIndex) {
helpers.insert(currentIndex + 1, { name: "", value: "" });
}

Swapping Items

function moveUp(index) {
if (index > 0) {
helpers.swap(index, index - 1);
}
}

function moveDown(index) {
if (index < helpers.items.length - 1) {
helpers.swap(index, index + 1);
}
}

Using getItemFieldProps

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

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

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>
);
}