A lightweight, flexible, and type-safe React form validation hook with zero dependencies (except React)
npm install react-validate-hook> A lightweight, flexible, and type-safe React form validation hook with zero dependencies (except React).



In the landscape of React form validation libraries, most solutions force you into one of two extremes:
1. Heavy frameworks (react-hook-form, Formik) - Full form management with opinionated state control
2. Schema-only validators (Yup, Zod standalone) - No React integration, manual glue code required
react-validate-hook fills the gap between these extremes with a validation-first approach:
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā
ā Heavy Form Frameworks ā
ā āā Form state management (controlled/uncontrolled)ā
ā āā Field registration ā
ā āā Validation engine āāāā You want this ā
ā āā Submit handling ā
ā āā Reset/dirty tracking ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
vs.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā
ā react-validate-hook ā
ā āā Validation engine only ā
ā āā Render prop pattern ā
ā āā Type-safe generics ā
ā āā Schema adapter pattern ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Perfect for:
- Custom form architectures - You control state, this handles validation
- Incrementally validating existing forms - Drop in ValidateWrapper without refactoring
- Multi-step wizards - Validate individual steps independently
- Non-form validation - Validate any user input (search, filters, configuration)
- Design system builders - Provide validation as a primitive, not a framework
Consider alternatives if you need:
- Full form state management ā Use react-hook-form or FormikFinal Form
- Complex field dependencies ā Use with field-level subscriptionsRemix
- Server-side validation only ā Use form actions or Next.js server actionsuseState
- No validation at all ā Use controlled components with
---
`bash`
npm install react-validate-hookor
yarn add react-validate-hookor
pnpm add react-validate-hook
Peer Dependencies:
- react: ^18.0.0
---
For straightforward validation logic without schemas:
`tsx
import { useValidator } from 'react-validate-hook';
function LoginForm() {
const { ValidateWrapper, validate, errors, reset } = useValidator();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
validate();
if (errors.length === 0) {
// Submit form
}
};
return (
$3
For complex validation rules using Zod, Yup, or custom schemas:
`tsx
import { useValidator } from 'react-validate-hook';
import { z } from 'zod';const emailSchema = z.string().email("Invalid email");
const passwordSchema = z.string().min(8, "Min 8 characters");
function SignupForm() {
const { ValidateWrapper, validate, errors } = useValidator(
(data, schema) => {
const result = schema.safeParse(data);
return result.success ? true : result.error.errors[0].message;
}
);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
);
}
`$3
Support any validation library by writing a thin adapter:
`tsx
// Yup Adapter
import * as Yup from 'yup';const yupAdapter = (data: any, schema: Yup.AnySchema) => {
try {
schema.validateSync(data);
return true;
} catch (error) {
return error.message;
}
};
const { ValidateWrapper } = useValidator(yupAdapter);
// Joi Adapter
import Joi from 'joi';
const joiAdapter = (data: any, schema: Joi.Schema) => {
const result = schema.validate(data);
return result.error ? result.error.message : true;
};
// Custom Validator
const customAdapter = (data: any, rules: ValidationRules) => {
// Your custom validation logic
return rules.validate(data) ? true : rules.getError();
};
`---
API Reference
$3
Create a validator with inline validation functions.
Returns:
SimpleValidatorReturn
- ValidateWrapper - Component wrapper for validated fields
- validate() - Trigger validation for all wrapped fields
- reset() - Clear validation state and errors
- errors - Array of current error messages---
$3
Create a validator with schema-based validation.
Parameters:
-
validationFactory: (value, schema) => ValidationResult - Factory function to validate values against schemasReturns:
FactoryValidatorReturn
- ValidateWrapper - Component wrapper accepting schema in fn prop
- validate() - Trigger validation
- reset() - Clear state
- errors - Current errors---
$3
#### Common Props (Both Modes)
| Prop | Type | Required | Description |
|------|------|----------|-------------|
|
setValue | (value: T) => void | ā
Yes | Callback to update parent state |
| value | T | ā No | Optional initial/external value for validation |
| children | Render function | ā
Yes | Receives { error, setValue } or { error, value, setValue } |Important: The
children callback signature changes based on whether value is provided:
- Without value prop: ({ error, setValue }) => ReactNode
- With value prop: ({ error, value, setValue }) => ReactNode#### Simple Mode Additional Props
| Prop | Type | Description |
|------|------|-------------|
|
fn | (value) => ValidationResult | Validation function |#### Factory Mode Additional Props
| Prop | Type | Description |
|------|------|-------------|
|
fn | TSchema | Schema object (e.g., Zod schema) |---
Advanced Patterns
$3
Validate each step independently:
`tsx
function Wizard() {
const step1Validator = useValidator();
const step2Validator = useValidator();
const [step, setStep] = useState(1); const nextStep = () => {
if (step === 1) {
step1Validator.validate();
if (step1Validator.errors.length === 0) setStep(2);
}
};
return (
<>
{step === 1 && (
)}
{step === 2 && (
)}
>
);
}
`$3
Enable/disable validation based on conditions:
`tsx
function ConditionalForm() {
const { ValidateWrapper, validate } = useValidator();
const [isRequired, setIsRequired] = useState(false); return (
<>
setValue={setFieldValue}
fn={(value) => {
if (!isRequired) return true;
return value ? true : "This field is required";
}}
>
{({ error, setValue }) => (
setValue(e.target.value)} />
)}
>
);
}
`$3
When you have existing values (e.g., editing existing data), use the optional
value prop to ensure validation has access to the current value from the start:`tsx
function EditProfile() {
const { ValidateWrapper, validate } = useValidator();
// Existing data from API/props
const [username, setUsername] = useState("john_doe");
const [email, setEmail] = useState("john@example.com"); const handleSave = () => {
validate();
// Validation works correctly even on initial load
};
return (
);
}
`Type Safety: TypeScript enforces that when you provide
value, your children callback must accept it:`tsx
// ā
Correct - no value prop, callback doesn't use it
{({ error, setValue }) => setValue(e.target.value)} />}
// ā
Correct - value prop provided, callback uses it
{({ error, value, setValue }) => setValue(e.target.value)} />}
// ā TypeScript Error - value prop provided but callback doesn't accept it
{({ error, setValue }) => }
// ā TypeScript Error - no value prop but callback tries to use it
{({ error, value, setValue }) => }
`$3
Both validation functions and factory adapters support async operations:
`tsx
// Async inline validation
setValue={setUsername}
fn={async (value) => {
if (!value) return "Required";
const available = await checkUsernameAvailability(value);
return available ? true : "Username taken";
}}
>
{({ error, setValue }) => (
setValue(e.target.value)} />
)}
// Async factory validation
const asyncAdapter = async (data: any, schema: z.ZodType) => {
await simulateNetworkDelay(100);
const result = schema.safeParse(data);
return result.success ? true : result.error.errors[0].message;
};
const { ValidateWrapper, validate } = useValidator(asyncAdapter);
// All validations complete before validate() resolves
await validate();
if (errors.length === 0) {
// Safe to submit
}
`The
validate() function returns a Promise that resolves only after all async validations complete, making it safe to check errors immediately after.---
Comparison with Alternatives
| Feature | react-validate-hook | react-hook-form | Formik | Final Form |
|---------|---------------------|-----------------|--------|------------|
| Bundle Size | ~1.2KB | ~9KB | ~13KB | ~5KB |
| Form State | ā You control | ā
Built-in | ā
Built-in | ā
Built-in |
| Validation Only | ā
Core focus | ā Coupled | ā Coupled | ā Coupled |
| Schema Support | ā
Any via adapter | ā
Zod/Yup | ā
Yup | ā ļø Custom |
| Type Safety | ā
Full generics | ā
Good | ā ļø Moderate | ā ļø Moderate |
| Learning Curve | Low | Moderate | Moderate | High |
| Render Props | ā
Yes | ā Ref-based | ā ļø Limited | ā
Yes |
$3
Use react-validate-hook when:
- Building a custom form library or design system
- Need validation in non-form contexts (filters, search, config)
- Want minimal bundle impact with maximum flexibility
- Already have state management (Redux, Zustand, Context)
Use react-hook-form when:
- Building standard CRUD forms quickly
- Want performant uncontrolled forms
- Need battle-tested DevTools integration
Use Formik when:
- Migrating from class components
- Need Formik's ecosystem (plugins, integrations)
- Prefer explicit form-level state
Use Final Form when:
- Need fine-grained field-level subscriptions
- Complex multi-step forms with field dependencies
- Want framework-agnostic core (also works with Vue, Angular)
---
TypeScript Support
Fully typed with generics for maximum type safety:
`tsx
// Type-safe value inference
const { ValidateWrapper } = useValidator(); // Explicitly typed
setValue={setAge}
fn={(value) => {
//
value is number | undefined | null
if (!value) return "Required";
if (value < 18) return "Must be 18+";
return true;
}}
>
{({ error, setValue }) => {
// setValue accepts number
return setValue(+e.target.value)} />
}}
// Type-safe schemas
const { ValidateWrapper } = useValidator((data: User, schema: z.ZodType) => {
return schema.safeParse(data).success ? true : "Invalid user";
});
`---
Contributing
Contributions welcome!
$3
`bash
Install dependencies
npm installRun tests
npm testRun tests in watch mode
npm run test:watchBuild
npm run build
``---
MIT Ā© [Kabui Charles]