GitHub
Open Github

UseForm

Lightweight form state and validation management for React.

Installation

pnpm add @hookraft/useform

useForm

import { useForm } from "@hookraft/useform"
const form = useForm({
  fields: {
    email: {
      value: "",
      rules: [
        { required: true, message: "Email is required" },
        { pattern: /^\S+@\S+\.\S+$/, message: "Invalid email address" },
      ],
    },
    password: {
      value: "",
      rules: [
        { required: true, message: "Password is required" },
        { minLength: 8, message: "Must be at least 8 characters" },
      ],
    },
  },
  onSubmit: async (values) => await loginUser(values),
  onSuccess: () => console.log("Logged in!"),
  onError: (err) => console.error("Failed:", err),
})

The problem it solves

Every form in React makes you write the same boilerplate — field state, error state, loading state, validation logic, submit handling.

// Without useForm
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isSubmitting, setIsSubmitting] = useState(false)

// With useForm
const form = useForm({
  fields: {
    email: { value: "", rules: [{ required: true }] },
    password: { value: "", rules: [{ required: true }] },
  },
})

How it works

Each field exposes everything you need directly:

form.fields.email.value
form.fields.email.error
form.fields.email.touched
form.fields.email.isDirty
form.fields.email.isValid
form.fields.email.disabled

Connecting inputs:

<input
  value={form.fields.email.value as string}
  onChange={(e) => form.setValue("email", e.target.value)}
  onBlur={() => form.setTouched("email")}
/>

{form.fields.email.error && <p>{form.fields.email.error}</p>}

Full Example

import { useForm } from "@hookraft/useform"

function LoginForm() {
  const form = useForm({
    fields: {
      email: {
        value: "",
        rules: [
          { required: true, message: "Email is required" },
          { pattern: /^\S+@\S+\.\S+$/, message: "Enter a valid email" },
        ],
      },
      password: {
        value: "",
        rules: [
          { required: true, message: "Password is required" },
          { minLength: 8, message: "Must be at least 8 characters" },
        ],
      },
    },
    validateOn: "blur",
    onSubmit: async (values) => {
      await fetch("/api/login", {
        method: "POST",
        body: JSON.stringify(values),
      })
    },
  })

  return (
    <form onSubmit={form.handleSubmit}>
      <input
        value={form.fields.email.value as string}
        onChange={(e) => form.setValue("email", e.target.value)}
        onBlur={() => form.setTouched("email")}
      />
      {form.fields.email.error && <p>{form.fields.email.error}</p>}

      <input
        type="password"
        value={form.fields.password.value as string}
        onChange={(e) => form.setValue("password", e.target.value)}
        onBlur={() => form.setTouched("password")}
      />
      {form.fields.password.error && <p>{form.fields.password.error}</p>}

      <button type="submit" disabled={form.isSubmitting}>
        {form.isSubmitting ? "Logging in..." : "Login"}
      </button>
    </form>
  )
}

Reset Example

Use a ref to call reset() from inside onSuccess:

import { useRef } from "react"
import { useForm } from "@hookraft/useform"

function ContactForm() {
  const formRef = useRef<useForm.Return<any>>(null!)

  const form = useForm({
    fields: {
      name: { value: "" },
      email: { value: "" },
      message: { value: "" },
    },
    onSubmit: async () => {},
    onSuccess: () => {
      formRef.current.reset()
    },
  })

  formRef.current = form

  return (
    <form onSubmit={form.handleSubmit}>
      <input
        value={form.fields.name.value as string}
        onChange={(e) => form.setValue("name", e.target.value)}
      />
    </form>
  )
}

Validation Rules

rules: [
  { required: true, message: "Required" },
  { minLength: 3, message: "Too short" },
  { maxLength: 50, message: "Too long" },
  { min: 0, message: "Must be positive" },
  { max: 100, message: "Must be 100 or less" },
  { pattern: /^\S+@\S+\.\S+$/, message: "Invalid email" },
  {
    validate: (value, allValues) => {
      if (value === allValues.username) return "Cannot match username"
      return true
    },
  },
]

Options

PropTypeDefault
fields
useForm.FieldsConfig
-
onSubmit
(values: useForm.FormValues) => Promise<void> | void
-
onSuccess
(values: useForm.FormValues) => void
-
onError
(error: unknown, values: useForm.FormValues) => void
-
onChange
(values: useForm.FormValues, fieldName: string) => void
-
validateOn
"change" | "blur" | "submit"
"blur"

Returns

PropTypeDefault
fields
{ [K in keyof T]: useForm.FieldState }
-
values
useForm.FormValues
-
errors
useForm.FormErrors
-
status
useForm.Status
-
is(status)
(s: useForm.Status) => boolean
-
isValid
boolean
-
isDirty
boolean
-
isSubmitting
boolean
-
isSuccess
boolean
-
isError
boolean
-
submitError
unknown
-
setValue(name, value)
(name: keyof T, value: useForm.FieldValue) => void
-
setTouched(name)
(name: keyof T) => void
-
setError(name, error)
(name: keyof T, error: string) => void
-
clearError(name)
(name: keyof T) => void
-
validateField(name)
(name: keyof T) => boolean
-
validateAll()
() => boolean
-
submit()
() => Promise<void>
-
handleSubmit(e?)
(e?: React.FormEvent) => Promise<void>
-
reset()
() => void
-
resetField(name)
(name: keyof T) => void
-