npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

formora

v0.6.2

Published

Type-safe, headless form state and validation for React

Readme

Formora

npm version CI License: MIT

Formora is a tiny, headless form state and validation hook for React.

📚 Documentation: https://narek-webdev.github.io/formora/


Formora helps you manage form values, track user interaction, and validate inputs using a clean, type-safe API — without enforcing any UI structure or UI framework.


✨ Features

  • ✅ Form state management (values)
  • ✅ Validation on change, blur, or submit
  • ✅ Built-in validation rules (required, pattern, minLength, maxLength, min, max)
  • ✅ Cross-field validation (v0.4)
  • ✅ Async validation (race-condition safe)
  • ✅ Debounced async validation (v0.2)
  • ✅ Field-level validating state
  • ✅ DX helpers: setValue, setValues, reset, resetField, error & touched helpers (v0.3)
  • ✅ Tracks errors, touched, and isValid
  • ✅ Fully controlled inputs
  • ✅ TypeScript-first, strongly typed field names
  • ✅ Headless (bring your own UI)
  • ✅ Nested object fields (v0.5)
  • ✅ Field arrays (append/remove) with dot-index paths (v0.6)

🤔 Why Formora?

Formora focuses on predictability over magic:

  • Validation timing is always explicit (change, blur, or submit)
  • Async validation is race-condition safe by default
  • Nested fields and field arrays behave consistently
  • The API stays small and composable

If you prefer full control over UI and validation flow, Formora is designed for you.


📦 Installation

npm install formora

or

yarn add formora

🚀 Basic Usage

import { useForm } from "formora";

function LoginForm() {
  const form = useForm({
    initialValues: { email: "" },
    validateOn: "submit",
  });

  return (
    <form onSubmit={form.handleSubmit(console.log)}>
      <input
        placeholder="Email"
        {...form.register("email", { required: "Email is required" })}
      />

      {form.touched.email && form.errors.email && <p>{form.errors.email}</p>}

      <button type="submit">Submit</button>
    </form>
  );
}

🧠 API Overview

useForm(options)

useForm<T>({
  initialValues: T,
  validateOn: "change" | "blur" | "submit",
  asyncDebounceMs: number,
  blockSubmitWhileValidating: boolean,
});

Returned values

| Property | Description | | -------------- | ------------------------------------------ | | values | Current form values | | errors | Validation errors per field | | touched | Tracks whether a field was blurred | | isValid | true if there are no errors | | register | Connects an input to the form | | handleSubmit | Handles submit + validation | | validating | Field-level async validation state | | isValidating | true if any field is validating | | submitCount | Number of submit attempts | | hasSubmitted | true after first submit attempt | | setValue | Programmatically set a field value | | reset | Reset the entire form to initial values | | resetField | Reset a single field to its initial value | | setValues | Programmatically set multiple field values | | setError | Manually set a field error | | clearError | Clear a specific field error | | clearErrors | Clear all field errors | | setTouched | Manually set a field's touched state | | touchAll | Mark all registered fields as touched |


register(name, rules)

<input {...form.register("email", { required: true })} />

Supported rules (v0.1)

  • required: boolean | string
  • pattern: RegExp | { value: RegExp; message: string }
  • minLength: number | { value: number; message: string }
  • maxLength: number | { value: number; message: string }
  • min: number | { value: number; message: string } (numbers / numeric strings)
  • max: number | { value: number; message: string } (numbers / numeric strings)
  • validate: (value, values) => string | undefined
  • validateAsync: (value, values) => Promise<string | undefined>

⏳ Async Validation (v0.2)

Formora treats async validation as a first-class feature.

validateAsync

register("email", {
  validateAsync: async (value) => {
    await new Promise((r) => setTimeout(r, 300));
    return value.includes("@") ? undefined : "Invalid email";
  },
});

Debounced async validation

You can debounce async validators globally:

useForm({
  initialValues: { email: "" },
  validateOn: "change",
  asyncDebounceMs: 300,
});

Or per field:

register("email", {
  validateAsync,
  asyncDebounceMs: 500,
});

UX rules

  • Async validation is race-condition safe (latest result always wins)
  • Debounce applies on change
  • blur and submit bypass debounce and run async validation immediately
  • validating[field] becomes true as soon as async validation is scheduled

🔗 Cross-field Validation (v0.4)

Formora supports cross-field validation, allowing a field’s validation logic to depend on the values of other fields.

Cross-field validation is explicit and predictable:

  • Validation logic belongs to the field where the rule is defined
  • Errors are assigned only to that field
  • On submit, all registered fields are validated, even if untouched

Example: Confirm password

register("confirmPassword", {
  validate: (value, values) =>
    value !== values.password ? "Passwords do not match" : undefined,
});

Example: Conditional required field

register("companyName", {
  validate: (value, values) =>
    values.isCompany && !value ? "Company name is required" : undefined,
});

Example: Date range validation

register("endDate", {
  validate: (value, values) => {
    if (!value || !values.startDate) return undefined;
    return new Date(value) <= new Date(values.startDate)
      ? "End date must be after start date"
      : undefined;
  },
});

Async cross-field validation

register("email", {
  validateAsync: async (value, values) => {
    await new Promise((r) => setTimeout(r, 300));
    return value !== values.confirmEmail ? "Emails do not match" : undefined;
  },
});

ℹ️ Formora does not auto-revalidate dependent fields when other fields change. This keeps validation behavior explicit and predictable.


🧬 Nested Fields (v0.5)

Formora supports nested object fields using dot-notation paths.

This allows you to work with real-world form shapes while keeping validation explicit and predictable.

Supported paths

"user.email";
"profile.address.street";

ℹ️ v0.5 introduced nested object paths. Field arrays (e.g. items.0.name) are supported starting in v0.6.


Basic example

const form = useForm({
  initialValues: {
    user: { email: "" },
    profile: { address: { street: "" } },
  },
});

<input {...form.register("user.email", { required: "Email is required" })} />
<input {...form.register("profile.address.street")} />

{form.errors.user?.email && <p>{form.errors.user.email}</p>}

Nested helpers

All helpers work with nested paths:

form.setValue("user.email", "[email protected]");
form.resetField("profile.address.street");
form.setTouched("user.email", true);
form.setError("user.email", "Invalid email");

Nested cross-field validation

Cross-field validation works seamlessly with nested values:

register("confirmPassword", {
  validate: (value, values) =>
    value !== values.user.password ? "Passwords do not match" : undefined,
});

Nested validation follows the same rules:

  • Validation is explicit
  • Errors are assigned only to the field being validated
  • On submit, all registered fields are validated


🧩 Field Arrays (v0.6)

Formora supports basic field arrays with append and remove helpers.

Array fields use dot-index paths (e.g. items.0.name). Bracket syntax like items[0].name is intentionally not supported.

Basic example

const form = useForm({
  initialValues: {
    items: [{ name: "" }],
  },
  validateOn: "submit",
});

function addItem() {
  form.append("items", { name: "" }, { shouldTouch: true });
}

function removeItem(i: number) {
  form.remove("items", i);
}

return (
  <form onSubmit={form.handleSubmit(console.log)}>
    {form.values.items.map((_, i) => (
      <div key={i}>
        <input
          {...form.register(`items.${i}.name`, {
            required: "Name required",
          })}
        />

        {form.errors.items?.[i]?.name && <p>{form.errors.items[i].name}</p>}

        <button type="button" onClick={() => removeItem(i)}>
          Remove
        </button>
      </div>
    ))}

    <button type="button" onClick={addItem}>
      Add item
    </button>

    <button type="submit">Submit</button>
  </form>
);

Behavior notes

  • append(name, value) adds an item to the end of the array.
  • remove(name, index) removes an item and shifts the array branch in errors, touched, and validating.
  • On submit, all registered fields (including array fields) are validated.

Async validation & field arrays

When removing an array item (e.g. remove("items", index)), Formora automatically cancels and invalidates any pending async validations for fields at or after the removed index.

This prevents stale async results (from old indices) from attaching to shifted items after removal.

Example scenario:

  • User types in items.2.email → async validation starts
  • User removes items.1
  • items.2.email becomes items.1.email
  • Formora cancels the old async validation so it cannot overwrite the new field state

🧰 DX Helpers (v0.3)

Formora provides a small set of developer experience helpers for common imperative form actions.

setValue(name, value, options?)

form.setValue("email", "[email protected]", {
  shouldTouch: true,
  shouldValidate: true,
});

Options:

  • shouldTouch — marks the field as touched
  • shouldValidate — runs sync + async validation immediately

This does not depend on validateOn and is fully explicit.


reset()

Resets the entire form to its initial state:

form.reset();

This will:

  • restore initialValues
  • clear errors, touched, and validating
  • cancel any pending async validation

Reset options

You can control which parts of the form state are preserved during reset:

form.reset({
  keepErrors: true,
  keepTouched: true,
  keepDirty: true,
  keepValidating: true,
});
  • keepErrors — preserve validation errors
  • keepTouched — preserve touched state
  • keepDirty — preserve dirty state
  • keepValidating — preserve async validating state (otherwise pending async is cancelled)

ℹ️ By default, reset() clears all meta state and cancels pending async validation.


resetField(name)

Resets a single field:

form.resetField("email");

This will:

  • restore the field’s initial value
  • clear its error, touched, and validating state
  • cancel pending async validation for that field

ResetField options

form.resetField("email", {
  keepError: true,
  keepTouched: true,
  keepDirty: true,
  keepValidating: true,
});
  • keepError — preserve the field error
  • keepTouched — preserve touched state for this field
  • keepDirty — preserve dirty state for this field
  • keepValidating — preserve async validating state for this field

When async validation is not preserved, any pending async validation for this field is cancelled and invalidated.


setValues(partial, options?)

Set multiple field values at once:

form.setValues(
  { email: "[email protected]", age: 25 },
  {
    shouldTouch: true,
    shouldValidate: true,
  }
);

Options:

  • shouldTouch — marks all provided fields as touched
  • shouldValidate — validates only the provided fields
  • bypassDebounce — if true, async validation runs immediately

Error helpers

form.setError("email", "Email already exists");
form.clearError("email");
form.clearErrors();

These helpers are useful for:

  • server-side validation errors
  • manual error control

Touched helpers

form.setTouched("email", true);
form.touchAll();
  • setTouched(name, boolean) manually toggles touched state
  • touchAll() marks all registered fields as touched

handleSubmit(onValid, onInvalid?)

<form onSubmit={form.handleSubmit(onValid, onInvalid)}>
  • Calls onValid(values) if form is valid
  • Calls onInvalid(errors) if validation fails

🧪 Testing

formora is tested using Vitest and Testing Library, focusing on real user behavior.

npm run test

🧩 Playground

This repository includes a small Vite + React playground used to manually test formora in a real application environment.

The playground demonstrates:

  • Sync validation rules
  • Async validation (race-condition safe)
  • Field-level validating state
  • Submit behavior while async validation is in progress

Run the playground locally

In one terminal (build the library in watch mode):

npm run build -- --watch

In another terminal (start the playground dev server):

cd playground
npm install
npm run dev

💡 The playground uses formora via a local file:.. dependency. If you change the library build output, you may need to reinstall dependencies inside playground/.


🎯 Design Principles

Formora is built with a few clear principles in mind:

  • Headless by design — Formora manages state and validation only. UI, layout, and styling are always up to you.
  • Predictable behavior — Validation timing is explicit (change, blur, or submit). No hidden side effects.
  • Async-first mindset — Async validation is treated as a first-class feature and is race-condition safe by default.
  • Type safety over magic — Strong TypeScript typing with explicit field names and minimal runtime assumptions.
  • Small, composable API — Fewer concepts, fewer surprises. Features are added deliberately.

🗺 Roadmap

Planned and possible future improvements:

  • ✅ Async validation UX improvements (debouncing, submit behavior)
  • ✅ Form state helpers (setValue, setValues, reset, resetField, error & touched helpers)
  • ✅ Cross-field validation
  • ✅ Nested object fields (v0.5)
  • ✅ Field arrays (append/remove) with dot-index paths (v0.6)
  • Improved playground examples

🤝 Contributing

Contributions are welcome!

  • Open issues for bugs or feature requests
  • Submit pull requests with clear descriptions
  • Keep changes small and well-tested

📄 License

MIT