Wit Form

useForm

useForm controls the form as a whole: what happens on submit, the initial values, form-level validation and schemas, the form state, and resetting. Call it once per form, in a component inside the FormProvider.

import { useForm } from 'wit-form';

const form = useForm<Values>(options);

The optional Values type types initialValues, onSubmit, onError and validate. To also check field names, use createFormHooks.

function SignupForm() {
  const { handleSubmit, formState } = useForm({
    initialValues: { plan: 'free' },
    onSubmit: async (values) => {
      await api.signUp(values);
    },
    onError: (fieldErrors) => console.warn(fieldErrors),
  });

  return (
    <form onSubmit={handleSubmit}>
      {/* fields */}
      <button disabled={formState.isSubmitting}>Sign up</button>
    </form>
  );
}

Options

onSubmit

Type: (values: Values, extraInfos?: any) => any · Required

Called by handleSubmit when every validation passes.

  • values: the form values, shaped by the field names.
  • extraInfos: the fields' extra infos, in the same shape. See Core Concepts.

If it returns a promise:

  • formState.isSubmitting is true until the promise settles.
  • When it resolves, the submitted values become the new initial values, so the form isn't dirty any more.
  • If it resolves to false, the initial values are left unchanged. Use this when a save fails and you want to keep the user's edits.
  • If it rejects, isSubmitting goes back to false and the error is ignored. Catch errors yourself to show them.

formState.isSubmitSuccessful is true afterwards unless it resolved to false or rejected.

const { setError, setFormErrors } = useForm({
  onSubmit: async (values) => {
    try {
      await api.save(values);
    } catch (err) {
      if (err.field) setError(err.field, err.message);
      else setFormErrors(['Could not save']);
      return false;
    }
  },
});

If it doesn't return a promise, the submitted values become the new initial values right away.

onError

Type: (fieldErrors, formErrors, values) => any · Default: none

Called instead of onSubmit when validation fails on submit.

ArgumentTypeDescription
fieldErrorsIFieldError[]One entry per failing field or field array. See below.
formErrorsstring[]What the form-level validate returned, plus form schema issues that don't belong to a field.
valuesanyThe values that failed validation.

Each IFieldError looks like:

{ name: 'email', ancestors: [], type: 'field', error: 'Required' }

initialValues

Type: object · Default: {}

The values the form starts with, in the same shape as the submitted values. Each field reads its starting value from here using its name.

Read once, when the form first mounts. To load values later, call resetInitialValues. See Loading and Saving Data.

By default, initial values for fields you never render are still included in the submitted values. Use skipUnusedInitialValues to leave them out.

validate

Type: (values: Values) => string[] | null | undefined | Promise<...> · Default: none

Form-level validation for rules that span several fields. Receives all values and returns a list of error messages. An empty list, null or undefined means valid. It can be async.

Runs only on submit, after field validation. The messages are passed to onError as formErrors, and kept in formState.formErrors until the next submit.

validate: (values) =>
  values.endDate < values.startDate ? ['End date is before start date'] : [];

A child component can replace this function with useSetFormProps.

schema

Type: StandardSchemaV1 · Default: none

A schema for all the form values, from any Standard Schema library: Zod, Valibot, ArkType, Effect Schema, Yup 1.7+ and others.

useForm({
  schema: z.object({ email: z.email(), age: z.number().min(18) }),
  onSubmit,
});
  • Each issue goes to the field at its path, including fields in field array rows (items[1].qty), or to the field array itself (items).
  • Issues without a matching field become form errors (formState.formErrors, and formErrors in onError).
  • The schema runs whenever a value changes, and on submit. Async schemas work too.
  • onSubmit receives the form's values, not the schema's parsed output.

Define it outside your component. See Schema validation.

mode

Type: 'onTouched' | 'onChange' | 'onSubmit' · Default: 'onTouched'

When field errors become visible. Validation runs on every change in all modes.

  • 'onTouched': after the field is blurred, or after a submit.
  • 'onChange': as soon as the user changes the field.
  • 'onSubmit': only after the first submit (or a validateFields call).

shouldFocusError

Type: boolean · Default: true

When a submit fails, focus the first invalid field on the page. Only fields whose ref is attached to an element can be focused.

skipUnregister

Type: boolean · Default: false

Normally, when a field unmounts, its edits are discarded. With skipUnregister: true, every field and field array in the form keeps its value when it unmounts. Use it for multi-step forms and tabs, where fields unmount but their values still matter.

To keep only some fields, pass skipUnregister to those useField or useFieldArray calls instead.

reinitializeOnSubmit

Type: boolean · Default: false

After a successful submit, reset the form to the original initialValues (or empty) instead of keeping the submitted values. Good for forms that should clear after each use, like "change password" or "post a comment".

skipUnusedInitialValues

Type: boolean · Default: false

Leave initial values that don't belong to a rendered field out of the submitted values. Without it, onSubmit receives the initial values merged with what the fields hold.

Use it when initialValues is a big record from your API and you only want to submit the fields the form shows.

Returns

handleSubmit

Type: (event?: FormEvent) => any

Validates the form and, if valid, calls onSubmit. Pass it to <form onSubmit>. It calls preventDefault() and stopPropagation() on the event, so the page doesn't reload. You can also call it without an event, for example from a button's onClick.

Returns whatever onSubmit returned, so you can await it. Returns undefined if validation failed. If any validator is async, it returns a promise that settles once validation and onSubmit are done.

formState

Type: IFormState

The form's state. Reading a property subscribes the component to it, and the component re-renders only when a property it read changes.

PropertyTypeDescription
isSubmittingbooleanonSubmit is running.
isSubmittedbooleanThe form has been submitted at least once, whether or not it was valid.
isSubmitSuccessfulbooleanThe last submit passed validation and onSubmit didn't resolve to false or reject.
submitCountnumberHow many times the form has been submitted.
isValidatingbooleanAn async validator is running, for a field, a field array or a submit.
isValidbooleanNo field or field array has an error, and the form schema passes.
isDirtybooleanThe values differ from the initial values. Same as useIsDirty.
errorsIFieldError[]Every current field and field array error, including ones not shown yet.
formErrorsstring[]Form-level errors from the last submit, or set with setFormErrors.
<button disabled={formState.isSubmitting || !formState.isValid}>Save</button>

Other components can read the same state with useFormState.

handleReset

Type: () => void

Resets every field and field array to the current initial values. Errors and touched state are cleared. After a successful submit, the "current initial values" are the submitted values.

resetInitialValues

Type: (values?: any, extraInfos?: any) => void

Replaces the initial values and resets every field to them. Call it when data arrives after the form mounted, like after a fetch. Called with no arguments, it keeps the current initial values and resets all fields to them.

useEffect(() => {
  api.getUser(id).then((user) => resetInitialValues(user));
}, [id]);

validateFields

Type: (names: (string | IFormContextFieldInput)[]) => IFieldError[]

Validates only the given fields or field arrays and returns their errors. An empty array means they're all valid. Failing fields are marked as touched so their errors show. Form schema issues for those fields are included.

Only sync validators are included in the result. Async validators start, and their errors show when they finish. To wait for them, use validateFieldsAsync.

  • A string is a top-level field name, or a top-level field array name.
  • For a field inside a row, pass { name, type: 'field', ancestors }.

The form-level validate doesn't run. Typical use: checking the current step of a wizard before moving on.

const next = () => {
  if (validateFields(['email', 'password']).length === 0) setStep(2);
};

validateFieldsAsync

Type: (names: (string | IFormContextFieldInput)[]) => Promise<IFieldError[]>

Like validateFields, but waits for async validators and returns all errors.

validateAllFields

Type: () => IFieldError[]

Validates every mounted field and field array, and the form schema, without submitting, and returns the errors. Failing fields are marked as touched. The form-level validate doesn't run. Like validateFields, only sync results are returned.

validateAllFieldsAsync

Type: () => Promise<IFieldError[]>

Like validateAllFields, but waits for async validators.

setError

Type: (key: string | IFormContextFieldInput, message: string) => void

Shows an error on a field (or field array) right away, for example one returned by your server. It stays until the field validates again, which happens when its value changes. See Errors from the server.

setError('email', 'This email is already registered');
setError(
  { name: 'qty', type: 'field', ancestors: [{ name: 'items', rowId }] },
  'Out of stock'
);

clearErrors

Type: (keys?: (string | IFormContextFieldInput)[]) => void

Clears the errors of the given fields. Without arguments, clears every field error and the form errors.

setFormErrors

Type: (messages: string[]) => void

Sets formState.formErrors, for errors that don't belong to a field.

setFocus

Type: (key: string | IFormContextFieldInput) => void

Focuses a field whose ref is attached.

getValues

Type: () => { values: any; extraInfos: any }

Returns the current values and extra infos. It reads them once and doesn't subscribe, so calling it doesn't cause re-renders. To show values that update live, use the watch hooks.

Notes

  • When the component that called useForm unmounts, the form's fields are reset.
  • useForm itself doesn't re-render when field values change. It only re-renders when a formState property it read changes.

On this page