v0.1Open source, MIT licensed

Fast React forms, one atom per field.

Wit Form is a headless form library that keeps every field in its own Jotai atom. Typing re-renders that one field, so forms with thousands of inputs stay fast without memoizing anything.

Get started
$ pnpm add wit-form jotai
  • ~7 kB gzipped
  • Jotai is the only dependency
  • TypeScript
  • React 18+
signup-form.tsxlive
<SignupForm />Form renders:0
Name renders:0
Email renders:0
useFormValues()Preview renders:0
{}

The counters show renders. Type in a field: only its counter, and the preview that watches it, go up.

Why atoms

Your form shouldn't re-render because one input changed.

Most form libraries keep values in one shared object, so every keystroke re-renders every field. Wit Form gives each field its own atom: an update only reaches the field that changed.

Edit any field in either form. Keystrokes go to both, and the counters are real React renders.

Shared form state0 renders
Name
Email
Phone
Company
Role
Website
Street
City
Postcode
Country
Team size

Every field wakes up

Each keystroke replaces the form object, so every field that reads it renders again, unless you memoize and tune selectors.

Wit Form0 renders
Name
Email
Phone
Company
Role
Website
Street
City
Postcode
Country
Team size

Only the field you touched

The input writes to its own atom. The rest of the form doesn't notice, with no memo, selectors or manual tuning.

Each form has 11 fields. Watch how many of them a single keystroke wakes up.

The API

A handful of hooks. Plain functions or your schema, no magic.

Wit Form manages state and validation. You keep full control of markup, styling and accessibility.

Bring your own inputs

useField hands you a value, a setter and an error. Wire them to any input, from a native element to your design system.

Read the guide
text-field.tsx
import { FormProvider, useField, useForm } from 'wit-form';

function TextField({ name, label }: { name: string; label: string }) {
  const { fieldValue, setFieldValue, onBlur, error } = useField<string>({
    name,
    validate: (value) => (!value ? 'Required' : null),
  });

  return (
    <label>
      {label}
      <input
        value={fieldValue ?? ''}
        onChange={(e) => setFieldValue(e.target.value)}
        onBlur={onBlur}
      />
      {error && <span role="alert">{error}</span>}
    </label>
  );
}

function Profile() {
  const { handleSubmit } = useForm({ onSubmit: save });
  return (
    <form onSubmit={handleSubmit}>
      <TextField name="name" label="Name" />
      <TextField name="address.city" label="City" />
      <button>Save</button>
    </form>
  );
}

export const ProfileForm = () => <FormProvider><Profile /></FormProvider>;

Features

Everything a production form needs. Nothing it doesn't.

Headless by design: Wit Form owns state and validation, and your components own the UI.

Field-level state

Each field is its own Jotai atom. Typing re-renders that field and nothing else, even in forms with thousands of inputs.

Live values without re-renders

Watch one field, one table column or the whole form. Only the component that watches re-renders.

Extra info per field

Store data next to a value, like a select option's label, and get it back in onSubmit.

Nested values from dot paths

Name a field address.city and submit gets { address: { city } }. No reshaping needed.

Field arrays

Add, insert, move, swap and remove rows, including lists inside lists, with stable row ids.

Validation at every level

Per field, per list and form-wide. Plain functions or any Standard Schema (Zod, Valibot, ArkType), sync or async with debouncing.

Headless, bring your own inputs

No UI ships with the library. Wire useField to native inputs or to your design system.

Real-world workflows

Initial values, form state flags, server errors, focus on the first invalid field, resets and multi-step wizards.

Small and type-safe

About 10 kB gzipped, ESM-only. createFormHooks type-checks every field name and value. React and Jotai are the only peers.

Built for big forms

A 500-row budget table that still types instantly.

Open the render-performance example and watch the counters: editing a cell re-renders that cell and its column total, while the other rows stay put.

Try the live benchmark
inputs in one form: 500 rows × 4 quarters
2,000
components re-render per edit: the cell and its total
2
selectors to write
0

FAQ

Questions, answered.

Something missing? Search the docs or start with the guide.

Do I need to know Jotai?

No. Jotai is the store under the hood, so you install it next to Wit Form, but you never create or read atoms yourself. You work with useForm, useField and the other hooks.

Will it clash with the Jotai store my app already uses?

No. Each FormProvider creates its own store, so forms stay isolated from your app state and from each other. If you want a form in a shared store, for example to read it from outside its provider, pass skipJotaiProvider: true. See FormProvider.

Can I use Zod, Yup or another validation library?

Yes. Pass any Standard Schema (Zod, Valibot, ArkType, Effect Schema, Yup 1.7+) as schema, for a field, a list or the whole form, with no adapter to install. Errors land on the matching fields. You can also call any library from a plain validator. See schema validation.

Does it ship input components?

No, it's headless. You write small field components with useField once, styled like the rest of your app, and reuse them in every form. They can wrap native inputs or your design system.

Which versions of React does it support?

React 18 or newer, with Jotai 3 or newer. The package is ESM-only and ships its own TypeScript types. In the Next.js App Router, use the hooks in client components (files with 'use client').

I use react-recoil-form. How do I switch?

Wit Form is its successor, with the same API on Jotai in place of Recoil. Swap the packages, change the imports, and remove any RecoilRoot you added only for forms. See the migration guide.

Build your first form in five minutes.

Install the package, wrap your form in a provider and call useField. That's the whole setup.

$ pnpm add wit-form jotai