FFormura

Form, Field & useFormState

Everything returned by createForm — the Form wrapper, the Field component, and the hook for reading submission state.

Form

Form is a <form> wrapper that handles validation and submission. It provides context for all <Field> components and the useFormState hook nested inside it.

PropTypeDescription
classNamestring?CSS class applied to the underlying <form> element.
childrenReactNode?Optional. Can contain <Field> overrides and any other elements (buttons, headings, dividers). Non-Field children are rendered after the auto-generated field rows.

With an adapter

When an adapter is configured, every schema key becomes an auto-rendered field. An empty <Form> with just a submit button is all you need:

const { Form, useFormState } = createForm({
  schema,
  adapter: shadcnAdapter,
  action,
  defaultValues: { username: "", email: "" },
});

const SignupForm = () => {
  const { isSubmitting } = useFormState();
  return (
    <Form className="space-y-4">
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Signing up..." : "Sign up"}
      </button>
    </Form>
  );
};

Without an adapter

Without an adapter, <Form> requires explicit <Field render={...}> children for every field — there is nothing to auto-render. TypeScript enforces this: omitting render is a compile error.

const { Form, Field } = createForm({ schema, action }); // no adapter

<Form>
  <Field
    name="username"
    render={({ field }) => (
      <input value={field.value as string} onChange={field.onChange} />
    )}
  />
  <button type="submit">Submit</button>
</Form>

Field

Field renders a single form field. When placed inside a <Form>, it overrides the auto-generated version for that schema key. The name prop is typed against your schema — TypeScript will catch typos or missing keys at compile time.

PropRequiredDescription
namealwaysThe schema key this field maps to. Typed as a union of your schema's top-level keys — invalid names are a TypeScript error.
labeloptionalDisplay label for this field. Overrides the auto-generated label. If omitted, the label is derived from the field name — firstName becomes First Name.
renderrequired without adapter/ optional with adapterCustom render function. Receives a fieldobject and returns JSX. When provided, it replaces the adapter's default widget for this field.
disabledoptionalPasses the disabled flag to the rendered input. The adapter and the render prop both receive it.
ErrorElementoptionalCustom component for displaying this field's error message. Overrides GlobalErrorElement for this field only. Receives errorMessage: string.

The render prop

The render function receives a field object with everything you need to wire any input:

PropertyDescription
nameThe field key string, e.g. "username".
valueCurrent field value, typed to the field's Zod output type.
onChangeCall with a new value or a DOM ChangeEvent — Formura extracts the value from either. For checkboxes, pass a boolean.
onBlurBlur handler. Reserved for future use — safe to forward.
disabledBoolean from the disabled prop.
<Field
  name="bio"
  label="About you"
  render={({ field }) => (
    <textarea
      value={field.value as string}
      onChange={field.onChange}
      disabled={field.disabled}
      rows={4}
    />
  )}
/>

Overriding one field

Place a <Field> inside the form when you want to override only one or two fields while leaving the rest auto-rendered by the adapter:

<Form className="space-y-4">
  {/* This field is customised — the rest auto-render normally */}
  <Field
    name="avatar"
    label="Profile photo"
    render={({ field }) => <AvatarUpload onChange={field.onChange} />}
  />
  <button type="submit">Save</button>
</Form>

useFormState

useFormState is a hook that exposes live submission state. It reads from the same context that <Form> provides — it must be called inside a component that is rendered within that <Form> tree.

PropertyTypeDescription
isSubmittingbooleantrue while the action is in flight. Goes back to false when the action resolves — or immediately if client-side validation fails (action was never called).
resultActionResult | nullThe last value returned by your action. null before the first submission. Contains status: "success" or status: "error" plus optional data or message.
submitCountnumberIncrements on every submit attempt, including ones that fail client-side validation. Useful for showing hints after multiple failed attempts.
signup-form.tsx
const { Form, useFormState } = createForm({ schema, adapter, action });

const SignupForm = () => {
  const { isSubmitting, result, submitCount } = useFormState();

  return (
    <Form className="space-y-4">
      {result?.status === "success" && (
        <p className="text-green-500">Account created!</p>
      )}
      {result?.status === "error" && result.message && (
        <p className="text-red-500">{result.message}</p>
      )}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Signing up..." : "Sign up"}
      </button>

      {submitCount > 2 && result?.status !== "success" && (
        <p className="text-sm text-muted-foreground">
          Having trouble? Check your input or try a different username.
        </p>
      )}
    </Form>
  );
};

useFormState can be called in any component rendered inside <Form> — it does not have to be in the same component that renders the form itself. This makes it easy to extract a submit button or a result banner into a separate component.

Override precedence

When multiple sources configure the same field, they merge from lowest to highest priority:

1

Auto-generated

Label derived from the field name, no overrides. This is the default when nothing else is specified.

2

fields option in createForm

Per-field config passed when calling createForm. Useful when the form factory lives in a separate module from the component.

3

<Field> JSX child

Explicit <Field name="..." /> inside the Form tree. Always wins over everything else for that field.

createForm({
  schema,
  adapter: shadcnAdapter,
  action,
  fields: {
    username: { label: "Handle" }, // priority 2 — overrides auto "Username"
  },
});

<Form>
  {/* priority 3 — wins over everything */}
  <Field name="username" label="Your unique handle" />
  <button type="submit">Submit</button>
</Form>

Error display

Field errors come from two sources: schema validation on submit, and fieldErrors returned by your action. Both map to the same per-field error state — your UI does not need to distinguish between them.

By default the adapter renders errors in its own style. You can replace the error display globally or per-field:

const MyError = ({ errorMessage }: { errorMessage: string }) => (
  <p className="mt-1 text-xs text-red-500">{errorMessage}</p>
);

createForm({
  schema,
  adapter: shadcnAdapter,
  action,
  GlobalErrorElement: MyError,       // default for all fields
  fields: {
    email: { ErrorElement: MyError }, // overrides GlobalErrorElement for email only
  },
});

Priority: Field prop ErrorElement > field config ErrorElement > GlobalErrorElement> adapter default.

Next

Widgets

How Zod types map to text, select, checkbox, date pickers, and more.