react-simple-form-hook
Version:
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
895 lines (707 loc) âĒ 25.5 kB
Markdown
# 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.
[](https://www.npmjs.com/package/react-simple-form-hook)
[](https://opensource.org/licenses/ISC)
## Table of Contents
- [Why react-simple-form-hook?](#why-react-simple-form-hook)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Features](#features)
- [Usage Examples](#usage)
- [Basic Example](#basic-example)
- [Advanced Example with Textarea](#advanced-example-with-textarea)
- [Using getFieldProps Helper](#using-getfieldprops-helper)
- [Programmatic Field Manipulation](#programmatic-field-manipulation)
- [Multi-Step Form Example](#multi-step-form-example)
- [API Reference](#api-reference)
- [Quick Reference](#quick-reference)
- [TypeScript Support](#typescript-support)
- [Validation](#validation)
- [FAQ](#faq)
- [Contributing](#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
```bash
npm install react-simple-form-hook zod
```
or with Yarn:
```bash
yarn add react-simple-form-hook zod
```
or with pnpm:
```bash
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:
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
{touched.email && errors.email && <span>{errors.email}</span>}
```
## Usage
### Basic Example
```tsx
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
```tsx
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:
```tsx
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
```tsx
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
```tsx
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:
```tsx
// 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](https://github.com/colinhacks/zod). You can use any Zod schema:
```tsx
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:**
```tsx
{touched.fieldName && errors.fieldName && <span>{errors.fieldName}</span>}
```
**Disable submit button while submitting:**
```tsx
<button type="submit" disabled={isSubmitting}>Submit</button>
```
**Disable submit button if form is invalid:**
```tsx
<button type="submit" disabled={!isValid}>Submit</button>
```
**Show loading state during submission:**
```tsx
<button type="submit">
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
```
**Reset button only enabled if form is dirty:**
```tsx
<button type="button" onClick={() => reset()} disabled={!isDirty}>
Reset
</button>
```
**Set field value programmatically:**
```tsx
setFieldValue('email', 'user@example.com');
```
**Validate specific field:**
```tsx
const isValid = validateField('email');
```
**Set multiple values at once:**
```tsx
setValues({
firstName: 'John',
lastName: 'Doe',
});
```
**Use getFieldProps for cleaner code:**
```tsx
<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
```bash
# 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
- [NPM Package](https://www.npmjs.com/package/react-simple-form-hook)
- [GitHub Repository](https://github.com/kartikkesbhat-2003/react-simple-form-hook)
- [Issue Tracker](https://github.com/kartikkesbhat-2003/react-simple-form-hook/issues)
- [Changelog](./CHANGELOG.md)
## Related Projects
- [Zod](https://github.com/colinhacks/zod) - TypeScript-first schema validation
- [React Hook Form](https://react-hook-form.com/) - Alternative form library
- [Formik](https://formik.org/) - 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`:
```tsx
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.