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
Maintainers
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.
Table of Contents
- Why react-simple-form-hook?
- Installation
- Quick Start
- Core Concepts
- Features
- Usage Examples
- API Reference
- Quick Reference
- TypeScript Support
- Validation
- FAQ
- Contributing
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 zodor with Yarn:
yarn add react-simple-form-hook zodor with pnpm:
pnpm add react-simple-form-hook zodNote: 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
- Default submit handler if none is provided to
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:booleantrueif form has been modified from initial values
isSubmitting:booleantruewhile form is being submitted (useful for async submissions)
isValid:booleantrueif 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
validateOnBluris 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
isSubmittingstate 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
trueif 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
- Helper that returns
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 | undefinedValidation
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 testLicense
ISC
Author
Kartik Kesbhat
Links
Related Projects
- Zod - TypeScript-first schema validation
- React Hook Form - Alternative form library
- Formik - Another popular form library
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.
