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

@railway-ts/use-form

v0.1.15

Published

React form hook that works with @railway-ts/pipelines to manage form state, validation, and submission.

Downloads

1,111

Readme

@railway-ts/use-form

npm version Build Status License: MIT TypeScript Coverage

Schema-first React forms with full TypeScript safety, composable validation, and native HTML bindings.

~3.6 kB minified + brotli

Why?

Most React form solutions split validation from types from bindings. You define a schema in one place, extract types in another, wire up resolvers in a third, and manually plumb errors into your UI. Every layer is a seam where things drift.

This library treats the schema as the single source of truth. One declaration gives you:

  • TypeScript types (inferred, never duplicated)
  • Validation (composable, accumulates all errors in one pass)
  • Field bindings (spread onto native HTML elements)
  • Error handling (three layers with deterministic priority)

Bring your own Zod, Valibot, or ArkType via Standard Schema, or use @railway-ts/pipelines natively for cross-field validation, targeted error placement, and Result types.

Design

  • Schema-driven -- define once, get types + validation + field paths
  • Three error layers -- client, field async, server -- with deterministic priority
  • Native HTML bindings -- spread onto inputs, selects, checkboxes, files, radios
  • Railway Result -- handleSubmit returns Result<T, E> for pattern matching

Install

bun add @railway-ts/use-form @railway-ts/pipelines  # or npm, pnpm, yarn

Requires React 18+ and @railway-ts/pipelines ^0.1.19.

Quick Start

import { useForm } from '@railway-ts/use-form';
import {
  object,
  string,
  required,
  chain,
  nonEmpty,
  email,
  type InferSchemaType,
} from '@railway-ts/pipelines/schema';

const loginSchema = object({
  email: required(chain(string(), nonEmpty('Email is required'), email())),
  password: required(chain(string(), nonEmpty('Password is required'))),
});

type LoginForm = InferSchemaType<typeof loginSchema>;

export function LoginForm() {
  const form = useForm<LoginForm>(loginSchema, {
    initialValues: { email: '', password: '' },
    onSubmit: async (values) => {
      console.log('Login:', values);
    },
  });

  return (
    <form onSubmit={(e) => void form.handleSubmit(e)}>
      <input type="email" {...form.getFieldProps('email')} />
      {form.touched.email && form.errors.email && (
        <span>{form.errors.email}</span>
      )}

      <input type="password" {...form.getFieldProps('password')} />
      {form.touched.password && form.errors.password && (
        <span>{form.errors.password}</span>
      )}

      <button type="submit" disabled={form.isSubmitting}>
        {form.isSubmitting ? 'Logging in...' : 'Log In'}
      </button>
    </form>
  );
}

Real-World Use Case

Registration form with cross-field validation (refineAt for password confirmation), per-field async validation (fieldValidators for username availability), and server errors -- all in one component:

import { useNavigate } from 'react-router-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from '@railway-ts/use-form';
import {
  object,
  string,
  required,
  chain,
  refineAt,
  nonEmpty,
  email,
  minLength,
  ROOT_ERROR_KEY,
  type InferSchemaType,
} from '@railway-ts/pipelines/schema';

// --- Schema ---

const schema = chain(
  object({
    username: required(chain(string(), nonEmpty(), minLength(3))),
    email: required(chain(string(), nonEmpty(), email())),
    password: required(chain(string(), nonEmpty(), minLength(8))),
    confirmPassword: required(chain(string(), nonEmpty())),
  }),
  refineAt(
    'confirmPassword',
    (d) => d.password === d.confirmPassword,
    'Passwords must match'
  )
);

type Registration = InferSchemaType<typeof schema>;

// --- API layer ---

const checkUsername = (username: string): Promise<{ available: boolean }> =>
  fetch(`/api/check-username?u=${encodeURIComponent(username)}`).then((res) =>
    res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`)
  );

class ApiValidationError extends Error {
  constructor(public fieldErrors: Record<string, string>) {
    super('Validation failed');
  }
}

const registerUser = async (values: Registration): Promise<void> => {
  const res = await fetch('/api/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(values),
  });

  if (!res.ok) throw new ApiValidationError(await res.json());
};

// --- Component ---

export function RegistrationForm() {
  const navigate = useNavigate();
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: registerUser,
    onSuccess: () => navigate('/welcome'),
    onError: (error) => {
      if (error instanceof ApiValidationError) {
        form.setServerErrors(error.fieldErrors);
      } else {
        form.setServerErrors({
          [ROOT_ERROR_KEY]: 'Network error. Please try again.',
        });
      }
    },
  });

  const form = useForm<Registration>(schema, {
    initialValues: {
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
    },
    fieldValidators: {
      username: async (value) => {
        try {
          const { available } = await queryClient.fetchQuery({
            queryKey: ['check-username', value],
            queryFn: () => checkUsername(value),
            staleTime: 30_000,
          });
          return available ? undefined : 'Username is already taken';
        } catch {
          return 'Unable to check username availability';
        }
      },
    },
    onSubmit: (values) => mutation.mutate(values),
  });

  return (
    <form onSubmit={(e) => void form.handleSubmit(e)}>
      <input {...form.getFieldProps('username')} />
      {form.validatingFields.username && <span>Checking...</span>}
      {form.touched.username && form.errors.username && (
        <span>{form.errors.username}</span>
      )}

      <input type="email" {...form.getFieldProps('email')} />
      {form.touched.email && form.errors.email && (
        <span>{form.errors.email}</span>
      )}

      <input type="password" {...form.getFieldProps('password')} />
      {form.touched.password && form.errors.password && (
        <span>{form.errors.password}</span>
      )}

      <input type="password" {...form.getFieldProps('confirmPassword')} />
      {form.touched.confirmPassword && form.errors.confirmPassword && (
        <span>{form.errors.confirmPassword}</span>
      )}

      {form.errors[ROOT_ERROR_KEY] && (
        <span>{form.errors[ROOT_ERROR_KEY]}</span>
      )}

      <button type="submit" disabled={mutation.isPending || form.isValidating}>
        {mutation.isPending ? 'Registering...' : 'Create Account'}
      </button>
    </form>
  );
}

Cross-field validation, async username check, server errors, and React Query integration -- production patterns, zero glue code.

What's Included

  • Type-safe field paths -- autocomplete for nested fields, dot-notation everywhere
  • Railway validation -- composable validators that accumulate all errors in one pass
  • Standard Schema v1 -- bring your own Zod, Valibot, or ArkType schema
  • Native HTML bindings -- spread getFieldProps onto inputs, selects, checkboxes, files, radios, sliders
  • Three error layers -- client, per-field async, and server errors with automatic priority
  • Array helpers -- type-safe push, remove, swap, move, insert, replace with field bindings
  • Four validation modes -- live, blur, mount, submit
  • Auto-submission -- useFormAutoSubmission for search/filter forms with debounced submit
  • React 18 + 19 -- compatible with both, tree-shakeable ESM

Works With

Any Standard Schema v1 library works out of the box -- no adapters, no wrappers. Pass the schema directly to useForm:

  • Zod 3.23+ (v4 also supported)
  • Valibot v1+
  • ArkType 2.0+
  • @railway-ts/pipelines (native)

See Recipes: Standard Schema for Zod and Valibot examples.

Ecosystem

@railway-ts/use-form is built on @railway-ts/pipelines -- composable, type-safe validation with Railway-oriented Result types. Use pipelines standalone for backend validation, or pair it with this hook for full-stack type safety.

Documentation

Examples

Clone and run:

git clone https://github.com/sakobu/railway-ts-use-form.git
cd railway-ts-use-form
bun install
bun run example

Then open http://localhost:3000. The interactive app has tabs for:

  • Sync -- Basic form with schema validation
  • Async (Cross-field) -- Async schema with cross-field rules (password confirmation, date ranges)
  • Zod -- Standard Schema integration with Zod
  • Valibot -- Standard Schema integration with Valibot
  • Field Validators -- Per-field async validation with loading indicators

Or try it live on StackBlitz.

Philosophy

  • Schema is the single source of truth
  • Validation should accumulate, not short-circuit
  • Types should be inferred, never duplicated
  • Form state should be explicit and predictable
  • Native HTML first, adapters never

Contributing

CONTRIBUTING.md

License

MIT © Sarkis Melkonian