@hookform/resolvers vs joi vs yup
Form Validation Strategies in React Applications
@hookform/resolversjoiyupSimilar Packages:

Form Validation Strategies in React Applications

yup and joi are schema validation libraries used to define rules for data structures, while @hookform/resolvers is an integration layer that connects these validators to React Hook Form. yup is designed with React in mind, offering a lightweight and fluent API for browser-based validation. joi is a robust, feature-rich validator often used on the server but adaptable for the client. @hookform/resolvers eliminates the need to write custom glue code, allowing React Hook Form to seamlessly consume schemas from yup, joi, or other libraries like Zod.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@hookform/resolvers02,2621.16 MB0a month agoMIT
joi021,1721.89 MB20115 days agoBSD-3-Clause
yup023,662270 kB255a year agoMIT

Form Validation Strategies in React Applications

Building reliable forms requires more than just checking if a field is empty. You need a system that handles complex rules, async checks, and user feedback without slowing down the interface. yup, joi, and @hookform/resolvers are key tools in this space, but they serve different roles. Let's break down how they work together and where they differ.

πŸ“ Defining Validation Rules: Schema Syntax

The way you write validation rules affects how easy your code is to read and change later.

yup uses a chainable API that reads like a sentence.

  • It is designed to be lightweight and works well in the browser.
  • Methods like .required() or .min() are attached directly to the type.
// yup: Schema definition
import * as yup from 'yup';

const schema = yup.object({
  username: yup.string().required('Name is required'),
  age: yup.number().min(18, 'Must be an adult')
});

joi also uses chaining but offers more built-in rules out of the box.

  • It is very strict and often used on the server.
  • The syntax is similar to yup but can be more verbose for complex cases.
// joi: Schema definition
import Joi from 'joi';

const schema = Joi.object({
  username: Joi.string().required().messages({ 'string.empty': 'Name is required' }),
  age: Joi.number().min(18).messages({ 'number.min': 'Must be an adult' })
});

@hookform/resolvers does not define rules itself.

  • Instead, it imports a specific resolver for the library you chose (like yup or joi).
  • This keeps your form logic separate from your validation logic.
// @hookform/resolvers: Importing the bridge
import { yupResolver } from '@hookform/resolvers/yup';
// OR
import { joiResolver } from '@hookform/resolvers/joi';

// You pass the schema defined above into this resolver
const resolver = yupResolver(schema); 

πŸ”Œ Connecting to React Hook Form

React Hook Form needs a way to run your schema against the form data. This is where integration matters.

yup is typically used with the resolver package.

  • You pass the yupResolver into the useForm hook.
  • This tells React Hook Form to run the yup schema on every change or submit.
// yup: Integration with React Hook Form
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';

const { register, handleSubmit } = useForm({
  resolver: yupResolver(schema)
});

joi follows the same pattern but uses the joiResolver.

  • The setup is identical, which makes swapping validators easy.
  • This consistency is a major benefit of using the resolver package.
// joi: Integration with React Hook Form
import { useForm } from 'react-hook-form';
import { joiResolver } from '@hookform/resolvers/joi';

const { register, handleSubmit } = useForm({
  resolver: joiResolver(schema)
});

@hookform/resolvers is the actual glue in both examples above.

  • Without this package, you would need to write a custom function to map errors.
  • It standardizes how external validators report errors to React Hook Form.
// @hookform/resolvers: The underlying mechanism
// This package exports the resolver functions used above
// It ensures error paths match React Hook Form's field names
export { yupResolver, joiResolver, zodResolver } from '@hookform/resolvers';

πŸ—£οΈ Custom Error Messages

Users need clear feedback when they make a mistake. Each library handles messages differently.

yup lets you pass messages directly into the validation methods.

  • You can override default text easily.
  • It supports dynamic messages using function arguments.
// yup: Custom messages
const schema = yup.object({
  email: yup.string()
    .email('Invalid email format')
    .required('Email is mandatory')
});

joi uses a .messages() object or specific method arguments.

  • It has a robust system for localization.
  • You can define a global message config for the whole schema.
// joi: Custom messages
const schema = Joi.object({
  email: Joi.string()
    .email()
    .required()
    .messages({
      'string.email': 'Invalid email format',
      'any.required': 'Email is mandatory'
    })
});

@hookform/resolvers passes these messages through unchanged.

  • It does not alter the text returned by yup or joi.
  • This ensures your validation library's features work as expected.
// @hookform/resolvers: Message passthrough
// The resolver captures the error object from yup/joi
// and maps it to React Hook Form's errors state
// No extra config needed for messages
const { errors } = useForm({ resolver: yupResolver(schema) });
// errors.email.message will contain 'Email is mandatory'

⏳ Async Validation Support

Sometimes you need to check a database (e.g., is the username taken?) before validating.

yup supports async tests using the .test() method.

  • You return a promise inside the test function.
  • React Hook Form waits for the promise to resolve before submitting.
// yup: Async validation
const schema = yup.object({
  username: yup.string().test(
    'is-unique',
    'Username taken',
    async (value) => {
      const response = await checkUsername(value);
      return response.available;
    }
  )
});

joi also supports async validation but requires enabling it.

  • You must pass { allowAsync: true } when validating.
  • The resolver handles this flag automatically for React Hook Form.
// joi: Async validation
const schema = Joi.object({
  username: Joi.string()
    .external(async (value) => {
      const response = await checkUsername(value);
      if (!response.available) throw new Error('Username taken');
      return value;
    })
});

@hookform/resolvers manages the async state for you.

  • It ensures the form shows a loading state while the check runs.
  • You don't need to manually toggle isSubmitting flags for validation.
// @hookform/resolvers: Async handling
// The resolver wraps the validation call
// React Hook Form handles the pending state automatically
const { formState: { isSubmitting } } = useForm({
  resolver: joiResolver(schema)
});
// isSubmitting is true while async test runs

🌍 Environment & Bundle Weight

Where your code runs matters. Some libraries are heavier than others.

yup is built for the browser.

  • It has a smaller footprint compared to joi.
  • Ideal for mobile users or strict performance budgets.
// yup: Browser optimization
// Import only what you need if using ESM
import { object, string } from 'yup';
// Tree-shaking friendly

joi is heavier and originally built for Node.js.

  • It works in the browser but adds more kilobytes to your bundle.
  • Best used when you share schemas between backend and frontend.
// joi: Bundle consideration
import Joi from 'joi';
// Full library is often pulled in
// Consider code-splitting if used only on specific admin pages

@hookform/resolvers is very lightweight.

  • It adds minimal overhead to your application.
  • The cost is negligible compared to the validation library you choose.
// @hookform/resolvers: Overhead
import { yupResolver } from '@hookform/resolvers/yup';
// Minimal code added to bundle
// Focus is on integration, not logic

🀝 Similarities: Shared Ground

Despite their differences, these tools work together to solve the same problems. Here are key overlaps:

1. πŸ”— Seamless React Hook Form Integration

  • All three are designed to work together in a React stack.
  • @hookform/resolvers ensures yup and joi speak the same language as the form.
// Shared pattern
const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: anyResolver(schema) // Works for yup, joi, zod, etc.
});

2. πŸ›‘οΈ Type Safety Support

  • Both yup and joi can infer TypeScript types from schemas.
  • @hookform/resolvers preserves these types in the form hooks.
// TypeScript inference
type FormData = yup.InferType<typeof schema>;
const { register } = useForm<FormData>({ ... });

3. πŸ”„ Reactive Error Updates

  • Errors update instantly as the user types (if configured).
  • No manual state management is needed for error messages.
// Reactive errors
<input {...register('username')} />
{errors.username && <span>{errors.username.message}</span>}

πŸ“Š Summary: Key Differences

Featureyupjoi@hookform/resolvers
Primary RoleπŸ“ Validation SchemaπŸ“ Validation SchemaπŸ”Œ Integration Bridge
Bundle Size⚑ Lightweight🐘 HeavierπŸͺΆ Minimal
Best Environment🌐 Browser / FrontendπŸ–₯️ Node / Full-stackβš›οΈ React Hook Form
Async Supportβœ… Via .test()βœ… Via .external()βœ… Auto-handled
Message ConfigπŸ—£οΈ Inline argumentsπŸ—£οΈ .messages() object➑️ Passthrough

πŸ“Š Summary: Key Similarities

FeatureShared by All Three
Ecosystemβš›οΈ React Hook Form compatible
TypeScriptπŸ›‘οΈ Full type inference
Error HandlingπŸ”„ Reactive updates
Maintenance🟒 Active community support

πŸ’‘ The Big Picture

yup is like a lightweight toolkit πŸŽ’ β€” perfect for frontend teams that want fast, readable validation without the bloat. It is the default choice for most React projects.

joi is like a heavy-duty safe 🏦 β€” ideal when you need strict rules that match your backend or require complex data transformations. Use it when consistency across server and client is a priority.

@hookform/resolvers is the adapter plug πŸ”Œ β€” you almost always need this if you use React Hook Form with an external validator. It saves you from writing boilerplate code and keeps your components clean.

Final Thought: For most new React applications, start with yup paired with @hookform/resolvers. Switch to joi only if you have a specific need for its advanced features or backend alignment. The resolver package is the key to making either choice feel effortless.

How to Choose: @hookform/resolvers vs joi vs yup

  • @hookform/resolvers:

    Choose @hookform/resolvers when you are already using React Hook Form and want to offload validation logic to an external schema library. It is the standard bridge that prevents you from writing manual validation wrappers, ensuring type safety and performance. This package is essential if you prefer defining rules outside of your component logic.

  • joi:

    Choose joi if your project requires complex validation rules that mirror a Node.js backend schema, or if you need to share validation logic between server and client. It is powerful and strict, making it suitable for enterprise applications where data integrity is critical. However, be mindful of its larger bundle size compared to alternatives when targeting mobile browsers.

  • yup:

    Choose yup if you want a validation library that feels native to the React ecosystem with a smaller footprint. It is ideal for forms where you need a fluent, chainable API that is easy to read and maintain. yup integrates effortlessly with @hookform/resolvers and is generally the go-to choice for new frontend-heavy projects.

README for @hookform/resolvers

Performant, flexible and extensible forms with easy to use validation.

npm downloads npm npm

React Hook Form Resolvers

This function allows you to use any external validation library such as Yup, Zod, Joi, Vest, Ajv and many others. The goal is to make sure you can seamlessly integrate whichever validation library you prefer. If you're not using a library, you can always write your own logic to validate your forms.

Install

Install your preferred validation library alongside @hookform/resolvers.

npm install @hookform/resolvers # npm
yarn add @hookform/resolvers # yarn
pnpm install @hookform/resolvers # pnpm
bun install @hookform/resolvers # bun
Resolver Comparison
resolverInfer values
from schema
criteriaMode
AJV❌firstError | all
ata-validator❌firstError | all
Arktypeβœ…firstError
class-validatorβœ…firstError | all
computed-typesβœ…firstError
Effectβœ…firstError | all
fluentvalidation-ts❌firstError
io-tsβœ…firstError
joi❌firstError | all
Nope❌firstError
Standard Schemaβœ…firstError | all
Superstructβœ…firstError
typanionβœ…firstError
typeboxβœ…firstError | all
typeschema❌firstError | all
valibotβœ…firstError | all
vest❌firstError | all
vineβœ…firstError | all
yupβœ…firstError | all
zodβœ…firstError | all

TypeScript

Most of the resolvers can infer the output type from the schema. See comparison table for more details.

useForm<Input, Context, Output>()

Example:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; // or 'zod/v4'

const schema = z.object({
  id: z.number(),
});

// Automatically infers the output type from the schema
useForm({
  resolver: zodResolver(schema),
});

// Force the output type
useForm<z.input<typeof schema>, any, z.output<typeof schema>>({
  resolver: zodResolver(schema),
});

Links

Table of Contents

API

type Options = {
  mode: 'async' | 'sync',
  raw?: boolean
}

resolver(schema: object, schemaOptions?: object, resolverOptions: Options)
typeRequiredDescription
schemaobjectβœ“validation schema
schemaOptionsobjectvalidation library schema options
resolverOptionsobjectresolver options, async is the default mode

Quickstart

Yup

Dead simple Object schema validation.

npm

⚠️ Pass context via useForm({ context }), not via yupResolver's schemaOptions. schemaOptions.context is overridden by the form context, so use the useForm context object instead.

// Correct
useForm({
  resolver: yupResolver(schema),
  context: { foo: true },
});

// Avoid - schemaOptions.context will be ignored/overridden
yupResolver(schema, { context: { foo: true } });
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup
  .object()
  .shape({
    name: yup.string().required(),
    age: yup.number().required(),
  })
  .required();

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: yupResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      <input type="number" {...register('age')} />
      <input type="submit" />
    </form>
  );
};

Zod

TypeScript-first schema validation with static type inference

npm

⚠️ Example below uses the valueAsNumber, which requires react-hook-form v6.12.0 (released Nov 28, 2020) or later.

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; // or 'zod/v4'

const schema = z.object({
  name: z.string().min(1, { message: 'Required' }),
  age: z.number().min(10),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};

⚠️ If your schema uses .default(...) on a field, that field becomes optional on the schema's input type (z.input) but stays required on its output type (z.output/z.infer) β€” the default only fills it in after validation. Passing a single generic to useForm<T> pins both to the same type and will conflict with zodResolver, which infers input and output separately. Either omit the generic and let it infer from resolver, or specify all three explicitly:

const schema = z.object({ debug_mode: z.boolean().default(true) });

useForm<z.input<typeof schema>, unknown, z.output<typeof schema>>({
  resolver: zodResolver(schema),
});

Superstruct

A simple and composable way to validate data in JavaScript (or TypeScript).

npm

import { useForm } from 'react-hook-form';
import { superstructResolver } from '@hookform/resolvers/superstruct';
import { object, string, number } from 'superstruct';

const schema = object({
  name: string(),
  age: number(),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: superstructResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      <input type="number" {...register('age', { valueAsNumber: true })} />
      <input type="submit" />
    </form>
  );
};

Joi

The most powerful data validation library for JS.

npm

import { useForm } from 'react-hook-form';
import { joiResolver } from '@hookform/resolvers/joi';
import Joi from 'joi';

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.number().required(),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: joiResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      <input type="number" {...register('age')} />
      <input type="submit" />
    </form>
  );
};

Vest

Vest 🦺 Declarative Validation Testing.

npm

import { useForm } from 'react-hook-form';
import { vestResolver } from '@hookform/resolvers/vest';
import { create, test, enforce } from 'vest';

const validationSuite = create((data = {}) => {
  test('username', 'Username is required', () => {
    enforce(data.username).isNotEmpty();
  });

  test('password', 'Password is required', () => {
    enforce(data.password).isNotEmpty();
  });
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: vestResolver(validationSuite),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

Class Validator

Decorator-based property validation for classes.

npm

⚠️ Remember to add these options to your tsconfig.json!

"strictPropertyInitialization": false,
"experimentalDecorators": true
import { useForm } from 'react-hook-form';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { Length, Min, IsEmail } from 'class-validator';

class User {
  @Length(2, 30)
  username: string;

  @IsEmail()
  email: string;
}

const resolver = classValidatorResolver(User);

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<User>({ resolver });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input type="text" {...register('username')} />
      {errors.username && <span>{errors.username.message}</span>}
      <input type="text" {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      <input type="submit" value="Submit" />
    </form>
  );
};

io-ts

Validate your data with powerful decoders.

npm

import React from 'react';
import { useForm } from 'react-hook-form';
import { ioTsResolver } from '@hookform/resolvers/io-ts';
import t from 'io-ts';
// you don't have to use io-ts-types, but it's very useful
import tt from 'io-ts-types';

const schema = t.type({
  username: t.string,
  age: tt.NumberFromString,
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: ioTsResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="number" {...register('age')} />
      <input type="submit" />
    </form>
  );
};

export default App;

Nope

A small, simple, and fast JS validator

npm

import { useForm } from 'react-hook-form';
import { nopeResolver } from '@hookform/resolvers/nope';
import Nope from 'nope-validator';

const schema = Nope.object().shape({
  name: Nope.string().required(),
  age: Nope.number().required(),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: nopeResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      <input type="number" {...register('age')} />
      <input type="submit" />
    </form>
  );
};

computed-types

TypeScript-first schema validation with static type inference

npm

import { useForm } from 'react-hook-form';
import { computedTypesResolver } from '@hookform/resolvers/computed-types';
import Schema, { number, string } from 'computed-types';

const schema = Schema({
  username: string.min(1).error('username field is required'),
  age: number,
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: computedTypesResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};

typanion

Static and runtime type assertion library with no dependencies

npm

import { useForm } from 'react-hook-form';
import { typanionResolver } from '@hookform/resolvers/typanion';
import * as t from 'typanion';

const isUser = t.isObject({
  username: t.applyCascade(t.isString(), [t.hasMinLength(1)]),
  age: t.applyCascade(t.isNumber(), [
    t.isInteger(),
    t.isInInclusiveRange(1, 100),
  ]),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: typanionResolver(isUser),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age')} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};

Ajv

The fastest JSON validator for Node.js and browser

npm

import { useForm } from 'react-hook-form';
import { ajvResolver } from '@hookform/resolvers/ajv';

// must use `minLength: 1` to implement required field
const schema = {
  type: 'object',
  properties: {
    username: {
      type: 'string',
      minLength: 1,
      errorMessage: { minLength: 'username field is required' },
    },
    password: {
      type: 'string',
      minLength: 1,
      errorMessage: { minLength: 'password field is required' },
    },
  },
  required: ['username', 'password'],
  additionalProperties: false,
};

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: ajvResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register('username')} />
      {errors.username && <span>{errors.username.message}</span>}
      <input {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}
      <button type="submit">submit</button>
    </form>
  );
};

TypeBox

JSON Schema Type Builder with Static Type Resolution for TypeScript

npm

With ValueCheck

import { useForm } from 'react-hook-form';
import { typeboxResolver } from '@hookform/resolvers/typebox';
import { Type } from '@sinclair/typebox';

const schema = Type.Object({
  username: Type.String({ minLength: 1 }),
  password: Type.String({ minLength: 1 }),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: typeboxResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

With TypeCompiler

A high-performance JIT of TypeBox, read more

import { useForm } from 'react-hook-form';
import { typeboxResolver } from '@hookform/resolvers/typebox';
import { Type } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';

const schema = Type.Object({
  username: Type.String({ minLength: 1 }),
  password: Type.String({ minLength: 1 }),
});

const typecheck = TypeCompiler.Compile(schema);

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: typeboxResolver(typecheck),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

Custom/third-party types (e.g. ElysiaJS's t.Files())

typeboxResolver validates using @sinclair/typebox's own Value.Errors/Value.Check, so any schema Kind not built into TypeBox (custom types, or ones defined by a third-party library such as ElysiaJS) must be registered with TypeBox's own TypeRegistry (and FormatRegistry for string formats) before it's used β€” this is a TypeBox-level extension point, not something @hookform/resolvers wraps or needs to expose separately:

import { TypeRegistry } from '@sinclair/typebox';

if (!TypeRegistry.Has('Files')) {
  TypeRegistry.Set('Files', (schema, value) => Array.isArray(value));
}

Make sure this registration runs against the same @sinclair/typebox module instance the schema was built with β€” if a library vendors its own copy of @sinclair/typebox instead of relying on the shared peer dependency, its registrations won't be visible here, and you'll see a runtime "Unknown type" error.

ArkType

TypeScript's 1:1 validator, optimized from editor to runtime

npm

import { useForm } from 'react-hook-form';
import { arktypeResolver } from '@hookform/resolvers/arktype';
import { type } from 'arktype';

const schema = type({
  username: 'string>1',
  password: 'string>1',
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: arktypeResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

Valibot

The modular and type safe schema library for validating structural data

npm

import { useForm } from 'react-hook-form';
import { valibotResolver } from '@hookform/resolvers/valibot';
import * as v from 'valibot';

const schema = v.object({
  username: v.pipe(
    v.string('username is required'),
    v.minLength(3, 'Needs to be at least 3 characters'),
    v.endsWith('cool', 'Needs to end with `cool`'),
  ),
  password: v.string('password is required'),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: valibotResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

⚠️ If your schema uses v.transform(...) (or v.pipe(..., v.transform(...))) on a field, that field's parsed output type can differ from what the user actually types (its input type) β€” e.g. a string input transformed into a number. Passing a single generic to useForm<T> pins both the submitted values and the handleSubmit payload to that one type, which conflicts with valibotResolver, since it infers input and output separately. Either omit the generic and let it infer from resolver, or specify all three explicitly:

const schema = v.object({
  name: v.string(),
  number: v.pipe(v.string(), v.transform(Number)),
});

useForm<v.InferInput<typeof schema>, unknown, v.InferOutput<typeof schema>>({
  resolver: valibotResolver(schema),
});

TypeSchema

Universal adapter for schema validation, compatible with any validation library

npm

import { useForm } from 'react-hook-form';
import { typeschemaResolver } from '@hookform/resolvers/typeschema';
import { z } from 'zod';

// Use your favorite validation library
const schema = z.object({
  username: z.string().min(1, { message: 'Required' }),
  password: z.number().min(1, { message: 'Required' }),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: typeschemaResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

effect-ts

A powerful TypeScript framework that provides a fully-fledged functional effect system with a rich standard library.

npm

import React from 'react';
import { useForm } from 'react-hook-form';
import { effectTsResolver } from '@hookform/resolvers/effect-ts';
import { Schema } from 'effect';

const schema = Schema.Struct({
  username: Schema.String.pipe(
    Schema.nonEmptyString({ message: () => 'username required' }),
  ),
  password: Schema.String.pipe(
    Schema.nonEmptyString({ message: () => 'password required' }),
  ),
});

type FormData = typeof schema.Type;

interface Props {
  onSubmit: (data: FormData) => void;
}

function TestComponent({ onSubmit }: Props) {
  const {
    register,
    handleSubmit,
    formState: { errors },
    // provide generic if TS has issues inferring types
  } = useForm<FormData>({
    resolver: effectTsResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username')} />
      {errors.username && <span role="alert">{errors.username.message}</span>}

      <input {...register('password')} />
      {errors.password && <span role="alert">{errors.password.message}</span>}

      <button type="submit">submit</button>
    </form>
  );
}

VineJS

VineJS is a form data validation library for Node.js

npm

import { useForm } from 'react-hook-form';
import { vineResolver } from '@hookform/resolvers/vine';
import vine from '@vinejs/vine';

const schema = vine.create({
  username: vine.string().minLength(1),
  password: vine.string().minLength(1),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: vineResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      {errors.username && <span role="alert">{errors.username.message}</span>}
      <input {...register('password')} />
      {errors.password && <span role="alert">{errors.password.message}</span>}
      <button type="submit">submit</button>
    </form>
  );
};

fluentvalidation-ts

A TypeScript-first library for building strongly-typed validation rules

npm

import { useForm } from 'react-hook-form';
import { fluentValidationResolver } from '@hookform/resolvers/fluentvalidation-ts';
import { Validator } from 'fluentvalidation-ts';

class FormDataValidator extends Validator<FormData> {
  constructor() {
    super();

    this.ruleFor('username')
      .notEmpty()
      .withMessage('username is a required field');
    this.ruleFor('password')
      .notEmpty()
      .withMessage('password is a required field');
  }
}

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: fluentValidationResolver(new FormDataValidator()),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      {errors.username && <span role="alert">{errors.username.message}</span>}
      <input {...register('password')} />
      {errors.password && <span role="alert">{errors.password.message}</span>}
      <button type="submit">submit</button>
    </form>
  );
};

standard-schema

A standard interface for TypeScript schema validation libraries

npm

Example zod

import { useForm } from 'react-hook-form';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1, { message: 'Required' }),
  age: z.number().min(10),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: standardSchemaResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};

Example arkType

import { useForm } from 'react-hook-form';
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema';
import { type } from 'arktype';

const schema = type({
  username: 'string>1',
  password: 'string>1',
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: standardSchemaResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('username')} />
      <input type="password" {...register('password')} />
      <input type="submit" />
    </form>
  );
};

ata-validator

A JSON Schema-based validator for JavaScript and TypeScript

npm

import { useForm } from 'react-hook-form';
import { ataResolver } from '@hookform/resolvers/ata-validator';

const schema = {
  type: 'object',
  properties: {
    username: {
      type: 'string',
      minLength: 1,
    },
    password: {
      type: 'string',
      minLength: 1,
    },
  },
  required: ['username', 'password'],
};

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: ataResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register('username')} />
      {errors.username && <span>{errors.username.message}</span>}
      <input {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}
      <button type="submit">submit</button>
    </form>
  );
};

Backers

Thanks go to all our backers! [Become a backer].

React Hook Form backers

Contributors

Thanks go to these wonderful people! [Become a contributor].