validact
v1.0.7
Published
The modern form validation hook for React. Add instant validation, error-handling micro-animations, and advanced debounce/throttle controls to any form. Lightweight, fast, and built for TypeScript.
Maintainers
Readme
Validact
A powerful, lightweight React form validation library with real-time validation, debounced input handling, and comprehensive form components.
Features
- 🚀 Real-time Validation - Validate fields as users type with 500ms debounce
- 📝 Multiple Input Types - Input, TextArea, RadioButton, DropDown, FilePicker, SubmitButton
- 🔒 Type Safety - Full TypeScript support with comprehensive type definitions
- 🎨 Customizable Styling - Tailwind CSS classes with custom style props
- 📁 File Upload Support - Single and multiple file upload with drag & drop
- ⚡ Lightweight - Minimal external dependencies
- 🔧 Flexible Validation - Built-in validators with custom validation support
- 🎯 Easy Integration - Simple API with unified form state management
- ✨ Schema-based - Explicit required/optional field control via schema
Installation
npm install validact
# or
yarn add validactQuick Start
import React from 'react';
import {
Input,
TextArea,
SubmitButton,
useFormValidation,
FormProvider
} from 'validact';
const MyForm = () => {
const initialValues = {
name: '',
email: '',
message: ''
};
const formApi = useFormValidation({ initialValues });
const onSubmit = (formData) => {
console.log('Form submitted:', formData);
};
return (
<FormProvider value={formApi}>
<form onSubmit={formApi.handleSubmit(onSubmit)} className="space-y-4">
{/* Required field - must specify optional: false */}
<Input
name="name"
label="Full Name"
schema={{ type: 'required', message: 'Name is required', optional: false }}
/>
{/* Required email field */}
<Input
name="email"
label="Email"
schema={{ type: 'email', message: 'Please enter a valid email', optional: false }}
/>
{/* Required textarea with min length */}
<TextArea
name="message"
label="Message"
schema={{ type: 'minLength', min: 10, message: 'Message must be at least 10 characters', optional: false }}
/>
<SubmitButton label="Submit Form" />
</form>
</FormProvider>
);
};Components
Input
Text input with validation support.
{/* With FormProvider - no need to pass value/onChange/onBlur/error */}
<Input
name="email"
label="Email Address"
schema={{ type: 'email', message: 'Please enter a valid email', optional: false }}
styleProps={{
container: "mb-4",
label: "text-sm font-medium",
input: "px-3 py-2 border rounded"
}}
/>
{/* Without FormProvider - pass all props manually */}
<Input
name="email"
label="Email Address"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
error={errors.email}
schema={{ type: 'email', message: 'Please enter a valid email', optional: false }}
/>TextArea
Multi-line text input with validation.
<TextArea
name="message"
label="Your Message"
schema={{ type: 'minLength', min: 10, message: 'Message must be at least 10 characters', optional: false }}
/>RadioButton
Radio button group with validation.
<RadioButton
name="gender"
label="Gender"
options={["Male", "Female", "Other"]}
schema={{ type: 'required', message: 'Please select a gender', optional: false }}
/>DropDown
Searchable dropdown with validation.
<DropDown
name="country"
label="Country"
options={["USA", "Canada", "UK", "Germany"]}
/>FilePicker
File upload with drag & drop and validation.
<FilePicker
name="resume"
label="Upload Resume"
accept=".pdf,.doc,.docx"
maxSize={5 * 1024 * 1024} // 5MB
multiple={true}
dragAndDrop={true}
showFileInfo={true}
schema={{
type: 'file',
allowedTypes: ['.pdf', '.doc', '.docx'],
message: 'Please upload a valid document',
optional: false // Required
}}
/>SubmitButton
Form submission button with validation state.
<SubmitButton
label="Submit Form"
disabled={!isFormValid}
styleProps={{
base: "w-full px-4 py-2 rounded-lg font-medium"
}}
/>Validation Schemas
String Schemas (Shorthand)
Simple string schemas for quick validation (defaults to required):
schema="required" // Required field
schema="email" // Email validation
schema="phone" // Phone validation
schema="strongPassword" // Strong password validationObject Schemas (Explicit optional control)
When using object schema format, you must explicitly specify optional: true or optional: false:
// Required field with custom message
schema={{ type: 'required', message: 'This field is required', optional: false }}
// Required email with custom message
schema={{ type: 'email', message: 'Please enter a valid email', optional: false }}
// Required length validation
schema={{ type: 'minLength', min: 5, message: 'Must be at least 5 characters', optional: false }}
schema={{ type: 'maxLength', max: 100, message: 'Must be less than 100 characters', optional: false }}
// Optional field (validates format only when value provided)
schema={{ type: 'email', message: 'Invalid email', optional: true }}
// Required file validation
schema={{
type: 'file',
allowedTypes: ['.pdf', '.jpg', '.png'],
message: 'Please upload a valid file',
optional: false
}}
// Optional file validation
schema={{
type: 'file',
allowedTypes: ['.pdf', '.jpg', '.png'],
message: 'Please upload a valid file',
optional: true
}}Important: When using object schema format, the optional property is mandatory. This makes the API explicit and prevents confusion about field requirements.
Built-in Validators
required- Field is requiredemail- Valid email formatphone- Valid phone numberstrongPassword- Strong password requirementsminLength- Minimum character lengthmaxLength- Maximum character lengthfileRequired- File is requiredfileType- File type validationdateRequired- Valid date format
Custom Validation
You can create custom validation functions for fields that need specific validation logic beyond the built-in validators.
Creating Custom Validators
Custom validators must follow the ValidationFunction type:
import { ValidationFunction } from 'validact';
const validateAge: ValidationFunction = (value) => {
if (typeof value !== 'string') return 'Age must be a number';
const age = parseInt(value.trim());
if (isNaN(age)) return 'Please enter a valid age';
if (age < 18) return 'You must be at least 18 years old';
if (age > 120) return 'Please enter a realistic age';
return null; // No error
};Using Custom Validators
Pass your custom validation function directly to the onBlur handler or via formApi.handleBlur:
{/* With FormProvider */}
<Input
name="age"
label="Age"
onBlur={(name, value) => formApi.handleBlur(name, value, validateAge)}
/>
{/* Without FormProvider */}
<Input
name="age"
label="Age"
value={formData.age}
onChange={handleChange}
onBlur={(name, value) => handleBlur(name, value, validateAge)}
error={errors.age}
/>Custom Validation Features
- On Blur Validation: Validates when user leaves the field
- Debounced Validation: Validates while typing with 500ms delay
- Real-time Error Display: Shows errors immediately below the input
- Form Submission Validation: Validates again on form submit
Custom Validation Examples
// Age validation (18-120 years)
const validateAge: ValidationFunction = (value) => {
if (typeof value !== 'string') return 'Age must be a number';
const age = parseInt(value.trim());
if (isNaN(age)) return 'Please enter a valid age';
if (age < 18) return 'You must be at least 18 years old';
if (age > 120) return 'Please enter a realistic age';
return null;
};
// Username validation (alphanumeric, 3-20 chars)
const validateUsername: ValidationFunction = (value) => {
if (typeof value !== 'string') return 'Username must be text';
const trimmed = value.trim();
if (trimmed.length < 3) return 'Username must be at least 3 characters';
if (trimmed.length > 20) return 'Username must be less than 20 characters';
if (!/^[a-zA-Z0-9_]+$/.test(trimmed)) return 'Username can only contain letters, numbers, and underscores';
return null;
};
// Custom email domain validation
const validateCompanyEmail: ValidationFunction = (value) => {
if (typeof value !== 'string') return 'Email must be text';
const trimmed = value.trim();
if (!/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(trimmed)) {
return 'Please enter a valid email address';
}
if (!trimmed.endsWith('@company.com')) {
return 'Email must be from @company.com domain';
}
return null;
};Required vs Optional Fields
Schema-Based Control (Recommended)
When using object schema format, you must explicitly specify optional: true or optional: false. This makes field requirements clear and self-documenting:
// Required field
<Input
name="email"
label="Email"
schema={{ type: 'email', message: 'Please enter a valid email', optional: false }}
/>
// Optional field (validates format only when value provided)
<Input
name="website"
label="Website (Optional)"
schema={{ type: 'email', message: 'Please enter a valid URL', optional: true }}
/>Optional Field Behavior
- Empty optional fields (
optional: true): No validation errors shown - Non-empty optional fields: Validated according to their schema
- Required fields (
optional: false): Show "Field is required" error when empty - String shorthand schemas (e.g.,
schema="email"): Treated as required by default
Legacy fieldConfigs Support
The library still supports the legacy fieldConfigs parameter for backward compatibility:
const formApi = useFormValidation({
initialValues: {
name: '',
email: '',
website: ''
},
fieldConfigs: {
website: { optional: true } // Legacy way to make field optional
}
});However, using optional in the schema object is the recommended approach as it's more explicit and co-locates the requirement with the validation rule.
Form State Management
The useFormValidation hook provides:
const formApi = useFormValidation({
initialValues: {
name: '',
email: '',
website: ''
},
// Optional: legacy fieldConfigs (not recommended - use schema.optional instead)
fieldConfigs: {
website: { optional: true }
}
});
// formApi contains:
formApi.values // Current form values
formApi.errors // Validation errors
formApi.handleChange // Handle input changes
formApi.handleBlur // Handle input blur (can pass custom validator)
formApi.handleSubmit // Handle form submission
formApi.isFormValid // Form validation state
formApi.validateAllFields // Manual validationUsing FormProvider (Recommended)
Wrap your form with FormProvider to avoid manually passing props to each component:
<FormProvider value={formApi}>
<form onSubmit={formApi.handleSubmit(onSubmit)}>
{/* Components automatically consume formApi - no need to pass values/errors/handlers */}
<Input name="email" label="Email" schema={{ type: 'email', optional: false }} />
</form>
</FormProvider>Complete Example with Custom Validation & Optional Fields
Here's a complete, copy-paste ready example showing:
- ✅ Custom validation function
- ✅ Required fields with
optional: false - ✅ Optional fields with
optional: true - ✅ FormProvider pattern (no need to pass values/errors/handlers to each component)
import React from 'react';
import {
Input,
TextArea,
RadioButton,
DropDown,
FilePicker,
SubmitButton,
useFormValidation,
FormProvider,
ValidationFunction
} from 'validact';
const ContactForm = () => {
// Custom validation function for age
const validateAge: ValidationFunction = (value) => {
if (typeof value !== 'string') return 'Age must be a number';
const age = parseInt(value.trim());
if (isNaN(age)) return 'Please enter a valid age';
if (age < 18) return 'You must be at least 18 years old';
if (age > 120) return 'Please enter a realistic age';
return null;
};
const initialValues = {
fullName: '',
email: '',
age: '',
website: '', // Optional field
gender: '',
country: '',
resume: null,
message: ''
};
const formApi = useFormValidation({ initialValues });
const onSubmit = (formData) => {
console.log('Form submitted:', formData);
alert('Form submitted! Check console for data.');
};
return (
<FormProvider value={formApi}>
<form onSubmit={formApi.handleSubmit(onSubmit)} className="space-y-4">
{/* Required field - explicit optional: false */}
<Input
name="fullName"
label="Full Name"
schema={{ type: 'required', message: 'Name is required', optional: false }}
/>
{/* Required email field */}
<Input
name="email"
label="Email Address"
schema={{ type: 'email', message: 'Please enter a valid email', optional: false }}
/>
{/* Required field with CUSTOM validation - pass validator to handleBlur */}
<Input
name="age"
label="Age"
onBlur={(name, value) => formApi.handleBlur(name, value, validateAge)}
/>
{/* OPTIONAL field - only validates format when value is provided */}
<Input
name="website"
label="Website (Optional)"
schema={{ type: 'email', message: 'Please enter a valid URL', optional: true }}
/>
{/* Required radio button */}
<RadioButton
name="gender"
label="Gender"
options={["Male", "Female", "Other", "Prefer not to say"]}
schema={{ type: 'required', message: 'Please select a gender', optional: false }}
/>
{/* Dropdown (required by default) */}
<DropDown
name="country"
label="Country"
options={["United States", "Canada", "United Kingdom", "India"]}
/>
{/* File picker - required */}
<FilePicker
name="resume"
label="Upload Resume"
accept=".pdf,.doc,.docx"
maxSize={10 * 1024 * 1024} // 10MB
multiple={false}
dragAndDrop={true}
schema={{
type: 'file',
allowedTypes: ['.pdf', '.doc', '.docx'],
message: 'Please upload a valid document',
optional: false
}}
/>
{/* Required textarea with min length */}
<TextArea
name="message"
label="Message"
schema={{ type: 'minLength', min: 10, message: 'Message must be at least 10 characters', optional: false }}
/>
<SubmitButton label="Submit Contact Form" />
</form>
</FormProvider>
);
};Styling
All components support custom styling through styleProps:
<Input
styleProps={{
container: "mb-4",
label: "text-sm font-medium text-gray-700",
input: "px-3 py-2 border border-gray-300 rounded-md",
errorText: "text-red-500 text-sm mt-1"
}}
/>TypeScript Support
Full TypeScript support with comprehensive type definitions:
import {
FormValue,
InputProps,
ValidationFunction,
useFormValidation,
FormProvider
} from 'validact';License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Key Changes in Latest Version
Schema-Based Optional Control
- Object schemas now require explicit
optional: boolean- prevents ambiguity about field requirements - Schema-based approach is more maintainable than separate
fieldConfigs - Legacy
fieldConfigsstill supported for backward compatibility
Example Migration
Old way (still works):
const formApi = useFormValidation({
initialValues: { website: '' },
fieldConfigs: { website: { optional: true } }
});
<Input name="website" schema={{ type: 'email' }} />New way (recommended):
const formApi = useFormValidation({
initialValues: { website: '' }
});
<Input
name="website"
schema={{ type: 'email', message: 'Invalid email', optional: true }}
/>Changelog
Latest
- Schema-based optional control (object schemas require
optional: boolean) - FormProvider pattern for cleaner component APIs
- Enhanced TypeScript type safety
- Improved documentation with copy-paste examples
1.0.0
- Initial release
- Real-time validation with debounce
- Multiple form components
- File upload support
- TypeScript support
- Customizable styling
