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 🙏

© 2025 – Pkg Stats / Ryan Hefner

react-simple-form-hook

v2.0.0

Published

A lightweight, type-safe React form management hook with built-in Zod validation, field-level validation, touched state tracking, async submissions, and advanced form manipulation features

Readme

react-simple-form-hook

A lightweight, type-safe React form management hook with built-in Zod validation, field-level validation, touched state tracking, async submissions, and advanced form manipulation features.

npm version License: ISC

Table of Contents


Why react-simple-form-hook?

Unlike other form libraries that can be bloated or overly complex, react-simple-form-hook provides a perfect balance between simplicity and power:

  • 🚀 Quick to learn - Simple API, start building forms in minutes
  • 💪 Powerful features - Everything you need for complex forms
  • 🔒 Type-safe - Full TypeScript support with automatic type inference
  • 📦 Small bundle - Minimal dependencies (only React and Zod)
  • Performance optimized - Uses React hooks best practices
  • 🎯 Zero configuration - Works out of the box with sensible defaults

Features

  • 🎯 Type-safe: Full TypeScript support with type inference
  • Zod Validation: Built-in schema validation using Zod
  • 🪶 Lightweight: Minimal dependencies and small bundle size
  • 🎨 Simple API: Intuitive and easy-to-use interface
  • 🔄 Form Reset: Built-in reset functionality
  • 📝 Error Handling: Automatic error management per field
  • 🎭 Touched State: Track which fields have been interacted with
  • 🔍 Field-level Validation: Validate individual fields on change or blur
  • 💾 Dirty State: Know if the form has been modified
  • Async Submit: Built-in support for async form submissions
  • 🎛️ Field Manipulation: Programmatically set values, errors, and touched state
  • 📦 getFieldProps Helper: Quickly spread props to form fields
  • ✔️ Form Validity: Real-time form validation status
  • 🎚️ Flexible Validation: Choose when to validate (onChange, onBlur, or onSubmit)
  • 🔧 Select & Checkbox Support: Works with all input types

Installation

npm install react-simple-form-hook zod

or with Yarn:

yarn add react-simple-form-hook zod

or with pnpm:

pnpm add react-simple-form-hook zod

Note: Zod is a peer dependency and must be installed separately.

Quick Start

Get your first form running in under 2 minutes:

import { useForm } from 'react-simple-form-hook';
import { z } from 'zod';

// 1. Define your validation schema
const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(6, 'Min 6 characters'),
});

// 2. Create your form component
function LoginForm() {
  const { values, errors, touched, handleChange, handleSubmit } = useForm({
    initialValues: { email: '', password: '' },
    schema,
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input name="email" value={values.email} onChange={handleChange} />
      {touched.email && errors.email && <span>{errors.email}</span>}
      
      <input name="password" type="password" value={values.password} onChange={handleChange} />
      {touched.password && errors.password && <span>{errors.password}</span>}
      
      <button type="submit">Login</button>
    </form>
  );
}

That's it! You now have a fully validated form with error handling. 🎉

Core Concepts

1. Schema-First Approach

Define your validation rules once using Zod, and react-simple-form-hook handles the rest:

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

2. Type Safety

TypeScript automatically infers your form types from the schema:

type FormData = z.infer<typeof schema>;
// FormData is { username: string; email: string; age: number }

const form = useForm<FormData>({ initialValues, schema });
// form.values is fully typed!

3. Smart Validation

Control when validation happens:

useForm({
  initialValues,
  schema,
  validateOnChange: false, // Don't validate while typing (default)
  validateOnBlur: true,    // Validate when field loses focus (default)
});

4. Touched State

Only show errors after users interact with fields:

{touched.email && errors.email && <span>{errors.email}</span>}

Usage

Basic Example

import { useForm } from 'react-simple-form-hook';
import { z } from 'zod';

// Define your form schema
const loginSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(6, 'Password must be at least 6 characters'),
});

type LoginForm = z.infer<typeof loginSchema>;

function LoginForm() {
  const { 
    values, 
    errors, 
    touched,
    isDirty,
    isSubmitting,
    handleChange, 
    handleSubmit, 
    reset 
  } = useForm<LoginForm>({
    initialValues: {
      email: '',
      password: '',
    },
    schema: loginSchema,
    validateOnBlur: true, // Validate when field loses focus
  });

  const onSubmit = (data: LoginForm) => {
    console.log('Form submitted:', data);
    // Handle your form submission here
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
        {touched.email && errors.email && (
          <span className="error">{errors.email}</span>
        )}
      </div>

      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={values.password}
          onChange={handleChange}
        />
        {touched.password && errors.password && (
          <span className="error">{errors.password}</span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Login'}
      </button>
      <button type="button" onClick={() => reset()} disabled={!isDirty}>
        Reset
      </button>
    </form>
  );
}

Advanced Example with Textarea

import { useForm } from 'react-simple-form-hook';
import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
  subscribe: z.boolean().optional(),
});

type ContactForm = z.infer<typeof contactSchema>;

function ContactForm() {
  const { 
    values, 
    errors, 
    touched,
    handleChange, 
    handleBlur,
    handleSubmit, 
    isSubmitting,
    reset 
  } = useForm<ContactForm>({
    initialValues: {
      name: '',
      email: '',
      message: '',
      subscribe: false,
    },
    schema: contactSchema,
    validateOnBlur: true,
  });

  const onSubmit = async (data: ContactForm) => {
    // Async submission example
    const response = await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify(data),
      headers: { 'Content-Type': 'application/json' },
    });
    
    if (response.ok) {
      alert('Message sent successfully!');
      reset();
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          name="name"
          value={values.name}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.name && errors.name && (
          <span className="error">{errors.name}</span>
        )}
      </div>

      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && (
          <span className="error">{errors.email}</span>
        )}
      </div>

      <div>
        <label htmlFor="message">Message:</label>
        <textarea
          id="message"
          name="message"
          value={values.message}
          onChange={handleChange}
          onBlur={handleBlur}
          rows={5}
        />
        {touched.message && errors.message && (
          <span className="error">{errors.message}</span>
        )}
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            name="subscribe"
            checked={values.subscribe}
            onChange={handleChange}
          />
          Subscribe to newsletter
        </label>
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
      <button type="button" onClick={() => reset()}>Clear Form</button>
    </form>
  );
}

Using getFieldProps Helper

Simplify your code with the getFieldProps helper:

import { useForm } from 'react-simple-form-hook';
import { z } from 'zod';

const userSchema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
});

type UserForm = z.infer<typeof userSchema>;

function UserForm() {
  const { getFieldProps, errors, touched, handleSubmit, isValid } = useForm<UserForm>({
    initialValues: {
      username: '',
      email: '',
      role: 'user' as const,
    },
    schema: userSchema,
    validateOnChange: true, // Live validation
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <div>
        <input type="text" {...getFieldProps('username')} placeholder="Username" />
        {touched.username && errors.username && <span>{errors.username}</span>}
      </div>

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

      <div>
        <select {...getFieldProps('role')}>
          <option value="admin">Admin</option>
          <option value="user">User</option>
          <option value="guest">Guest</option>
        </select>
        {touched.role && errors.role && <span>{errors.role}</span>}
      </div>

      <button type="submit" disabled={!isValid}>Create User</button>
    </form>
  );
}

Programmatic Field Manipulation

import { useForm } from 'react-simple-form-hook';
import { z } from 'zod';

const schema = z.object({
  country: z.string(),
  state: z.string(),
  city: z.string(),
});

type LocationForm = z.infer<typeof schema>;

function LocationForm() {
  const { 
    values, 
    setFieldValue, 
    setFieldError,
    validateField,
    handleSubmit 
  } = useForm<LocationForm>({
    initialValues: { country: '', state: '', city: '' },
    schema,
  });

  const handleCountryChange = (country: string) => {
    setFieldValue('country', country);
    // Reset dependent fields
    setFieldValue('state', '');
    setFieldValue('city', '');
  };

  const handleStateChange = async (state: string) => {
    setFieldValue('state', state);
    setFieldValue('city', '');
    
    // Custom async validation example
    const isValidState = await fetch(`/api/validate-state?state=${state}`).then(r => r.json());
    if (!isValidState) {
      setFieldError('state', 'Invalid state for selected country');
    }
  };

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <select 
        value={values.country} 
        onChange={(e) => handleCountryChange(e.target.value)}
      >
        <option value="">Select Country</option>
        <option value="USA">USA</option>
        <option value="Canada">Canada</option>
      </select>

      <select 
        value={values.state} 
        onChange={(e) => handleStateChange(e.target.value)}
        disabled={!values.country}
      >
        <option value="">Select State</option>
        {/* Dynamic options based on country */}
      </select>

      <input 
        type="text" 
        value={values.city}
        onChange={(e) => setFieldValue('city', e.target.value)}
        onBlur={() => validateField('city')}
        disabled={!values.state}
        placeholder="City"
      />

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

Multi-Step Form Example

import { useState } from 'react';
import { useForm } from 'react-simple-form-hook';
import { z } from 'zod';

const registrationSchema = z.object({
  // Step 1
  email: z.string().email(),
  password: z.string().min(8),
  // Step 2
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  // Step 3
  address: z.string().min(5),
  phone: z.string().min(10),
});

type RegistrationForm = z.infer<typeof registrationSchema>;

function MultiStepForm() {
  const [step, setStep] = useState(1);
  
  const { 
    values, 
    errors, 
    touched,
    isDirty,
    handleChange,
    handleBlur,
    validateField,
    handleSubmit,
    setValues,
  } = useForm<RegistrationForm>({
    initialValues: {
      email: '',
      password: '',
      firstName: '',
      lastName: '',
      address: '',
      phone: '',
    },
    schema: registrationSchema,
  });

  const handleNext = () => {
    // Validate current step fields before proceeding
    if (step === 1) {
      const emailValid = validateField('email');
      const passwordValid = validateField('password');
      if (emailValid && passwordValid) setStep(2);
    } else if (step === 2) {
      const firstNameValid = validateField('firstName');
      const lastNameValid = validateField('lastName');
      if (firstNameValid && lastNameValid) setStep(3);
    }
  };

  const onSubmit = async (data: RegistrationForm) => {
    console.log('Complete registration:', data);
    // Submit to API
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {step === 1 && (
        <div>
          <h2>Step 1: Account</h2>
          <input {...{ name: 'email', value: values.email, onChange: handleChange, onBlur: handleBlur }} />
          {touched.email && errors.email && <span>{errors.email}</span>}
          
          <input type="password" {...{ name: 'password', value: values.password, onChange: handleChange, onBlur: handleBlur }} />
          {touched.password && errors.password && <span>{errors.password}</span>}
          
          <button type="button" onClick={handleNext}>Next</button>
        </div>
      )}

      {step === 2 && (
        <div>
          <h2>Step 2: Personal Info</h2>
          <input {...{ name: 'firstName', value: values.firstName, onChange: handleChange, onBlur: handleBlur }} />
          {touched.firstName && errors.firstName && <span>{errors.firstName}</span>}
          
          <input {...{ name: 'lastName', value: values.lastName, onChange: handleChange, onBlur: handleBlur }} />
          {touched.lastName && errors.lastName && <span>{errors.lastName}</span>}
          
          <button type="button" onClick={() => setStep(1)}>Back</button>
          <button type="button" onClick={handleNext}>Next</button>
        </div>
      )}

      {step === 3 && (
        <div>
          <h2>Step 3: Contact</h2>
          <input {...{ name: 'address', value: values.address, onChange: handleChange, onBlur: handleBlur }} />
          {touched.address && errors.address && <span>{errors.address}</span>}
          
          <input {...{ name: 'phone', value: values.phone, onChange: handleChange, onBlur: handleBlur }} />
          {touched.phone && errors.phone && <span>{errors.phone}</span>}
          
          <button type="button" onClick={() => setStep(2)}>Back</button>
          <button type="submit">Complete Registration</button>
        </div>
      )}
    </form>
  );
}

API Reference

useForm<TData>(props: UseFormProps<TData>): UseFormResult<TData>

Props

  • initialValues (required): TData

    • Initial values for your form fields
  • schema (required): ZodSchema<TData>

    • Zod schema for form validation
  • validateOnChange (optional): boolean (default: false)

    • Enable validation on every field change
  • validateOnBlur (optional): boolean (default: true)

    • Enable validation when a field loses focus
  • onSubmit (optional): (data: TData) => void | Promise<void>

    • Default submit handler if none is provided to handleSubmit

Returns

  • values: TData

    • Current form values
  • errors: Record<keyof TData, string | undefined>

    • Validation errors for each field
  • touched: Record<keyof TData, boolean>

    • Tracks which fields have been interacted with (focused and blurred)
  • isDirty: boolean

    • true if form has been modified from initial values
  • isSubmitting: boolean

    • true while form is being submitted (useful for async submissions)
  • isValid: boolean

    • true if all form values pass schema validation
  • handleChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void

    • Change handler for form inputs
    • Automatically updates values and clears errors
    • Supports text inputs, textareas, selects, and checkboxes
  • handleBlur: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void

    • Blur handler for form inputs
    • Marks field as touched and triggers validation if validateOnBlur is enabled
  • handleSubmit: (callback?: (data: TData) => void | Promise<void>) => (event: React.FormEvent) => Promise<void>

    • Submit handler that validates form before calling callback
    • Marks all fields as touched
    • Only executes callback if validation passes
    • Supports both sync and async callbacks
    • Sets isSubmitting state during execution
  • setFieldValue: <K extends keyof TData>(field: K, value: TData[K]) => void

    • Programmatically set the value of a specific field
  • setFieldError: <K extends keyof TData>(field: K, error: string | undefined) => void

    • Programmatically set or clear an error for a specific field
  • setFieldTouched: <K extends keyof TData>(field: K, touched: boolean) => void

    • Programmatically set the touched state of a specific field
  • validateField: <K extends keyof TData>(field: K) => boolean

    • Validate a single field and update its error state
    • Returns true if field is valid
  • reset: (newValues?: TData) => void

    • Resets form to initial values (or provided new values)
    • Clears all errors and touched states
    • Resets submitting state
  • clearErrors: () => void

    • Clears all validation errors
  • setValues: (values: Partial<TData>) => void

    • Set multiple field values at once
  • getFieldProps: <K extends keyof TData>(field: K) => FieldProps<TData[K]>

    • Helper that returns { name, value, onChange, onBlur } for a field
    • Simplifies spreading props to form inputs

TypeScript Support

This package is written in TypeScript and provides full type inference:

// Your form data type is automatically inferred
const { values, errors } = useForm({
  initialValues: {
    email: '',
    age: 0,
  },
  schema: mySchema,
});

// TypeScript knows the exact shape of values and errors
values.email // ✅ string
values.age   // ✅ number
errors.email // ✅ string | undefined

Validation

Validation is powered by Zod. You can use any Zod schema:

import { z } from 'zod';

const schema = z.object({
  username: z.string()
    .min(3, 'Username must be at least 3 characters')
    .max(20, 'Username must be less than 20 characters'),
  
  email: z.string().email('Invalid email address'),
  
  age: z.number()
    .min(18, 'Must be at least 18 years old')
    .max(120, 'Invalid age'),
  
  website: z.string().url('Invalid URL').optional(),
});

Quick Reference

Common Use Cases

Show error only after field is touched:

{touched.fieldName && errors.fieldName && <span>{errors.fieldName}</span>}

Disable submit button while submitting:

<button type="submit" disabled={isSubmitting}>Submit</button>

Disable submit button if form is invalid:

<button type="submit" disabled={!isValid}>Submit</button>

Show loading state during submission:

<button type="submit">
  {isSubmitting ? 'Submitting...' : 'Submit'}
</button>

Reset button only enabled if form is dirty:

<button type="button" onClick={() => reset()} disabled={!isDirty}>
  Reset
</button>

Set field value programmatically:

setFieldValue('email', '[email protected]');

Validate specific field:

const isValid = validateField('email');

Set multiple values at once:

setValues({
  firstName: 'John',
  lastName: 'Doe',
});

Use getFieldProps for cleaner code:

<input {...getFieldProps('username')} />
// Equivalent to:
// <input
//   name="username"
//   value={values.username}
//   onChange={handleChange}
//   onBlur={handleBlur}
// />

Requirements

  • React >= 16.8.0 (Hooks support required)
  • Zod >= 4.0.0

Browser Support

Works in all modern browsers that support ES2020. If you need to support older browsers, make sure your build pipeline includes appropriate transpilation.

TypeScript

This library is written in TypeScript and provides full type definitions out of the box. No need for @types packages!

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development

# Clone the repository
git clone https://github.com/yourusername/react-simple-form-hook.git

# Install dependencies
npm install

# Build the project
npm run build

# Run tests (if available)
npm test

License

ISC

Author

Kartik Kesbhat

Links

Related Projects

FAQ

Q: Why should I use this over React Hook Form or Formik?

A: If you prefer a simpler API with Zod validation built-in and want powerful features like field manipulation, touched state, and async handling without the complexity, this is for you.

Q: Can I use this without TypeScript?

A: Yes! While the library is written in TypeScript and provides excellent type safety, it works perfectly fine in plain JavaScript projects.

Q: Does it support array fields?

A: Currently, the library focuses on object forms. Array field support is planned for a future release.

Q: How do I integrate with my UI library?

A: The hook is UI-agnostic. Simply spread the field props or use getFieldProps() with your preferred component library (Material-UI, Ant Design, Chakra UI, etc.).

Q: Can I validate on submit only?

A: Yes! Set both validateOnChange and validateOnBlur to false:

useForm({
  initialValues,
  schema,
  validateOnChange: false,
  validateOnBlur: false,
});

The form will only validate when you call handleSubmit.

Support

If you find this library helpful, please consider:

  • ⭐ Starring the repository
  • 🐛 Reporting bugs
  • 💡 Suggesting new features
  • 📖 Improving documentation
  • 🔀 Contributing code

Made with ❤️ by developers, for developers.