UNPKG

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
# 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](https://img.shields.io/npm/v/react-simple-form-hook.svg)](https://www.npmjs.com/package/react-simple-form-hook) [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](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.