DEV Community

Sunil Reddy
Sunil Reddy

Posted on

Why are we still registering inputs in React? Introducing Native Form

Hey,

Like many of you, I've used React Hook Form and Formik across several production projects. While they're powerful, I always felt friction with how modern form libraries force you to treat standard HTML forms as legacy:

  • Having to manually register every input with refs: {...register("email")}
  • Wrapping UI components in custom controllers
  • Giant bundle sizes once you add schema resolvers and adapters

I wanted to see if we could go back to standard HTML5 forms as the single source of truth without sacrificing React ergonomics or type safety.

So I built Native Form:
👉 GitHub: https://github.com/sunilsv26/native-form
👉 npm: npm i @native-form/react

What makes it different?

  1. Zero Input Registration: You write plain HTML inputs with standard name attributes (<input name="email" />). Native Form discovers them automatically from the DOM via MutationObserver.
  2. Uncontrolled DOM-First Performance: Keystrokes don't trigger component re-renders. Your view only re-renders when form state (validation errors, submission status) actually changes.
  3. Microscopic Schema Adapters: Adapters for Zod, Yup, and Valibot are under 500 bytes. The core runtime has zero third-party dependencies.
  4. Built-in Multi-Step Memory: useWizard and preserveUnmountedValues: true keep your unmounted step values hydrated without complex state contexts.
  5. Framework Agnostic Under the Hood: The core runtime (@native-form/core) runs in pure Vanilla JS, Vue, or Svelte, while @native-form/react provides React 18 & 19 hooks (useForm, useField, useWatch, useWizard).

Quick Example:

import { useForm } from "@native-form/react";
import { zodResolver } from "@native-form/zod";
import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  age: z.number().min(18),
});

export function Form() {
  const { formRef, formState } = useForm({
    schema: zodResolver(schema),
    serializationMode: "typed", // automatically coerces type="number" and range to numbers
    onSubmit: (values) => console.log(values),
  });

  return (
    <form ref={formRef} noValidate>
      <input name="email" type="email" placeholder="Email" />
      {formState.errors.email && <span>{formState.errors.email}</span>}

      <input name="age" type="number" placeholder="Age" />
      {formState.errors.age && <span>{formState.errors.age}</span>}

      <button type="submit" disabled={formState.submitting}>Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

I recently released v0.1.2 with fixes for discrete controls, range sliders, and custom design system integration (like Material UI).

I'd love to hear your feedback on the architecture, API design, or any edge cases you'd like to see covered!

Top comments (0)