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

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.

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 validact

Quick 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 validation

Object 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 required
  • email - Valid email format
  • phone - Valid phone number
  • strongPassword - Strong password requirements
  • minLength - Minimum character length
  • maxLength - Maximum character length
  • fileRequired - File is required
  • fileType - File type validation
  • dateRequired - 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 validation

Using 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 fieldConfigs still 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