@letar/forms

Async Validation

Server-side async validation with debounce, cancellation, and caching

Overview

Async validation lets you check values against a server (e.g., email uniqueness, username availability) while the user types or when they leave a field.

Via Props

<Form.Field.String
  name="email"
  label="Email"
  asyncValidate={async (value) => {
    const exists = await fetch(`/api/check-email?email=${value}`)
    if (exists) return 'Email already registered'
  }}
  asyncDebounce={500}
  asyncTrigger="onBlur"
/>

Via Zod .meta()

const Schema = z.object({
  email: z.email().meta({
    asyncValidate: async (value) => {
      const res = await fetch(`/api/check-email?email=${value}`)
      const { exists } = await res.json()
      if (exists) return 'Email already registered'
    },
    asyncDebounce: 500,
    asyncTrigger: 'onBlur',
  }),
})

Props

All fields that use createField support these props:

PropTypeDefaultDescription
asyncValidate(value: unknown) => Promise<string | undefined>Validation function
asyncDebouncenumber500Debounce delay (ms)
asyncTrigger'onBlur' | 'onChange''onBlur'When to trigger

Features

  • Debounce — waits for user to stop typing before sending request
  • Request cancellation — AbortController cancels previous request when new input arrives
  • Caching — already validated values are not re-checked
  • Offline-safe — skips async validation when navigator.onLine is false
  • Props priority — props override schema meta config

useAsyncFieldValidation Hook

For custom field components:

import { useAsyncFieldValidation } from '@letar/forms'

const { validators, asyncDebounceMs, hasAsyncValidation } = useAsyncFieldValidation(
  schema,
  'email',
  { asyncValidate: myFn, asyncDebounce: 300 }
)

Live Example

Try the interactive example on forms-example.letar.best.

On this page