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

@airlib/react-form

v1.2.1

Published

Reactive and declarative React form components built on AIR Stack. Typed forms, field validation, cross-field matching, and array fields with zero boilerplate.

Readme

AIR React Form

Handling form states, validations, and complex data structures like arrays and nested objects in standard React can be verbose and hard to optimize for performance. @airlib/react-form provides reactive form components built on top of @anchorlib/react to solve this, ensuring high performance without unnecessary re-renders while giving a deep type-safe structure.

Creating Typed Forms

Building robust forms requires strict schema validation and type safety. Here how we define a schema and create a typed form.

import { z } from 'zod';
import { createForm } from '@airlib/react-form';

const userSchema = z.object({
  name: z.string().min(3),
  email: z.string().email(),
});

export const UserForm = createForm(userSchema);

The createForm function returns form components (Form, Field, FieldList) typed against the provided Zod schema, ensuring autocompletion and compile-time checks for all field names.

Building Form Interfaces

Building the UI requires binding inputs to the form state. The typed form provides everything needed to structure the form with type safety.

import { UserForm } from './form';
import { TextInput, EmailInput, FormSubmit, FormReset } from '@airlib/react-form';

export function ProfileEditor() {
  return (
    <UserForm onSubmit={(data) => console.log(data)}>
      <UserForm.Field name="name" label="Name" errorClass="text-red-500">
        <TextInput placeholder="Enter name" />
      </UserForm.Field>

      <UserForm.Field name="email" label="Email" errorClass="text-red-500">
        <EmailInput placeholder="Enter email" />
      </UserForm.Field>

      <div>
        <FormReset>Reset</FormReset>
        <FormSubmit>Save Profile</FormSubmit>
      </div>
    </UserForm>
  );
}

The UserForm.Field wrapper tracks errors and provides them to the UI, while components like TextInput and EmailInput connect to the form state under the hood. The FormSubmit and FormReset buttons track form changes and validation status, enabling or disabling based on the form's readiness.

Cross-Field Matching

Fields that must match another field (like confirm password) use the match prop.

const passwordSchema = z.object({
  password: z.string().min(6),
  confirmPassword: z.string().min(6),
});

const PasswordForm = createForm(passwordSchema);

export function ChangePassword() {
  return (
    <PasswordForm onSubmit={save}>
      <PasswordForm.Field name="password" label="Password">
        <PasswordInput />
      </PasswordForm.Field>

      <PasswordForm.Field name="confirmPassword" match="password">
        {(field) => (
          <div>
            <PasswordInput />
            {field.touched && field.error?.map(err => <span key={err}>{err}</span>)}
            {!field.matched && <span>Passwords don't match</span>}
          </div>
        )}
      </PasswordForm.Field>

      <FormSubmit>Change Password</FormSubmit>
    </PasswordForm>
  );
}

The match prop accepts a field path for equality checks, or a function for custom cross-field logic. valid and matched are separate signals — valid is schema-only, matched is match-only — the view layer decides how to compose them.

For custom logic beyond equality:

<RangeForm.Field name="max" match={(form) => form.fields['max'] > form.fields['min']}>

Accessibility

Field and input components handle accessibility attributes out of the box:

  • <label> gets htmlFor linked to the input's auto-generated id
  • Error messages render with role="alert" and a stable id
  • Inputs get aria-invalid when errors exist
  • Inputs get aria-describedby pointing to the error element

Dot-path field names are sanitized to dashes for valid HTML ids (address.cityaddress-city).

Handling Form Arrays

Dealing with dynamic lists, such as arrays of objects, in a form is often complicated. The FieldList component abstracts this complexity.

import { z } from 'zod';
import { createForm, TextInput } from '@airlib/react-form';

const teamSchema = z.object({
  members: z.array(z.object({ name: z.string(), role: z.string() }))
});

const TeamForm = createForm(teamSchema);

export function TeamEditor() {
  return (
    <TeamForm>
      <TeamForm.FieldList name="members">
        {(items) => (
          <div>
            {items.map((member, i) => (
              <div key={i}>
                <TeamForm.Field name={`members.${i}.name`}>
                  <TextInput placeholder="Name" />
                </TeamForm.Field>
                <TeamForm.Field name={`members.${i}.role`}>
                  <TextInput placeholder="Role" />
                </TeamForm.Field>
              </div>
            ))}
            <button type="button" onClick={() => items.push({ name: '', role: '' })}>
              Add Member
            </button>
          </div>
        )}
      </TeamForm.FieldList>
    </TeamForm>
  );
}

The FieldList exposes the array items to the render function, allowing direct reactive mutations like .push() on the array. Because this uses @anchorlib/react under the hood, these mutations are tracked without the need for verbose state management hooks.

Working With Custom Inputs

Sometimes standard inputs are not enough. Building custom inputs that integrate with the form state is trivial using the built-in formInput hook or the createInput factory.

import { setup, render } from '@anchorlib/react';
import { formInput } from '@airlib/form';

export const CustomInput = setup<{ name: string }>((props) => {
  const input = formInput(props);

  return render(() => (
    <div>
      <input 
        name={input.name}
        value={input.value || ''} 
        onInput={(e) => input.value = e.currentTarget.value} 
        onBlur={() => input.settled()}
      />
      {input.error?.map(err => <span key={err} className="error">{err}</span>)}
    </div>
  ));
});

Using formInput() wires up the input state, validation rules, and error tracking based on the provided name prop. For simpler standard inputs, use the createInput('text') factory.