React Hook Form 16 ๐ Other Resolvers โ Yup, Joi, Superstruct, and Valibot integration
The @hookform/resolvers package is the single bridge between React Hook Form and the validation libraries, and the Zod chapter covered one of its adapters. The package ships the adapters for the several librariesโYup, Joi, Superstruct, Valibot, AJV, ArkType, TypeBox, and the othersโand each adapter follows the same contract: it receives the schema, validates the form values, and returns the normalized values and the errors in the shape that the formState expects . The choice of the library is the choice of the schema authoring style, and the adapter is the mechanism that makes the choice independent of the form’s code.
The four libraries in this chapter occupy the different points on the schema-authoring spectrum. Yup is the object-schema builder with the chainable methods, and it is the oldest of the four. Joi is the most powerful of the four, with the rich validation vocabulary and the descriptive error messages. Superstruct is the lightweight structural validator with the composable types. Valibot is the modular successor to Zod’s style, with the tree-shakeable functions and the pipe-based validation . The adapters are the mechanism that lets the form use any of them, and the differences are the authoring style, not the form integration.
Key point: The yupResolver, the joiResolver, the superstructResolver, and the valibotResolver are the adapters from the @hookform/resolvers package. Each adapter receives the schema and returns the resolver that the useForm accepts. The Yup, the Joi, the Superstruct, and the Valibot differ in the schema authoring style, the type inference, and the criteriaMode support, but the form’s integration code is the same: the resolver: libraryResolver(schema).
Why the resolver package exists
The adapter problem. Each validation library has its own API, its own error shape, and its own validation timing. The useForm accepts a single resolver function, and the function must return the values and the errors in the specific shape. The @hookform/resolvers package is the mechanism that translates each library’s output to that shape, and the translation is the reason the form does not need the library-specific code . Without the adapter, the developer would write the translation for each library, and the translation would drift.
The type-inference problem. The resolver can infer the form values type from the schema, and the inference is the mechanism that keeps the form and the schema aligned. The Yup, the Superstruct, and the Valibot infer the values, and the Joi does not . The inference is the reason the useForm without the explicit generic is still type-safe for the three libraries, and the explicit generic is the requirement for the Joi.
The criteriaMode problem. The criteriaMode option controls whether the resolver returns the first error per field or all the errors. The Yup, the Joi, and the Valibot support both the firstError and the all, and the Superstruct supports only the firstError . The difference is the mechanism that determines whether the UI can show the multiple messages for the same field, and the difference is the reason the library choice affects the error display.
The installation problem. The adapter is the separate package, and the schema library is the separate dependency. The installation is the npm install react-hook-form @hookform/resolvers <library>, and the three packages are the mechanism that makes the integration work . The adapter is the bridge, and the library is the schema engine.
a. The Yup and the Joi adapters
The Yup is the object-schema builder with the chainable methods. The yupResolver connects the Yup schema to the form, and the yup.InferType derives the TypeScript type.
// The Yup resolver
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
// The Yup schema with the chainable methods.
const schema = yup
.object({
name: yup.string().required("Name is required"),
email: yup.string().email("Enter a valid email").required(),
})
.required();
// The Yup type inference.
type FormValues = yup.InferType<typeof schema>;
function ContactForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
// The yupResolver connects the schema to the form.
resolver: yupResolver(schema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
{errors.name && <p role="alert">{errors.name.message}</p>}
<input {...register("email")} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<button type="submit">Send</button>
</form>
);
}
The yupResolver(schema) is the mechanism that connects the Yup schema to the useForm. The yup.InferType<typeof schema> is the mechanism that derives the form values type, and the type is the same as the schema’s output . The errors.name.message is the Yup error message, and the message is mapped to the formState.errors by the resolver.
The Yup context is the mechanism for the dynamic validation. The useForm‘s context option is passed to the schema, and the schema reads it through the $ context. The context is the mechanism that lets the validation depend on the external state, such as the user’s plan .
// The Yup context
const schema = yup.object({
email: yup.string().when("$plan", {
is: "paid",
then: (schema) => schema.email(),
otherwise: (schema) => schema,
}),
});
useForm<FormValues, { plan: "free" | "paid" }>({
resolver: yupResolver(schema),
context: { plan: "paid" },
});
The context: { plan: "paid" } is the mechanism that passes the context to the Yup schema, and the $plan is the mechanism that the schema reads it. The context is the reason the Yup can do the conditional validation, and the conditional is the reason the context exists.
The Joi is the most powerful of the four, and the joiResolver connects the Joi schema to the form. The Joi does not infer the values type, and the explicit generic is the requirement.
// The Joi resolver
import { useForm } from "react-hook-form";
import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";
// The Joi schema with the descriptive methods.
const schema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required(),
});
// The Joi does not infer the values type.
type FormValues = {
name: string;
age: number;
};
function ProfileForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
// The joiResolver connects the schema to the form.
resolver: joiResolver(schema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
{errors.name && <p role="alert">{errors.name.message}</p>}
<input type="number" {...register("age", { valueAsNumber: true })} />
{errors.age && <p role="alert">{errors.age.message}</p>}
<button type="submit">Save</button>
</form>
);
}
The joiResolver(schema) is the mechanism that connects the Joi schema to the form. The explicit FormValues type is the mechanism that types the form, because the Joi does not infer it . The valueAsNumber: true is the mechanism that converts the input string to the number before the Joi validation, and the conversion is the reason the Joi’s Joi.number() accepts the value.
b. The Superstruct and the Valibot adapters
The Superstruct is the lightweight structural validator with the composable types. The superstructResolver connects the Superstruct schema to the form, and the Infer utility derives the type.
// The Superstruct resolver
import { useForm } from "react-hook-form";
import { superstructResolver } from "@hookform/resolvers/superstruct";
import { object, string, number, Infer } from "superstruct";
// The Superstruct schema with the composable types.
const schema = object({
name: string(),
age: number(),
});
// The Superstruct type inference.
type FormValues = Infer<typeof schema>;
function ProfileForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
// The superstructResolver connects the schema to the form.
resolver: superstructResolver(schema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
{errors.name && <p role="alert">{errors.name.message}</p>}
<input type="number" {...register("age", { valueAsNumber: true })} />
{errors.age && <p role="alert">{errors.age.message}</p>}
<button type="submit">Save</button>
</form>
);
}
The superstructResolver(schema) is the mechanism that connects the Superstruct schema to the form. The Infer<typeof schema> is the mechanism that derives the type, and the type is the same as the schema’s output . The Superstruct supports only the firstError criteria mode, and the criteriaMode: "all" is the option that the Superstruct does not support .
The Valibot is the modular successor to the Zod style, and the valibotResolver connects the Valibot schema to the form. The Valibot uses the pipe for the validation chain, and the InferInput and the InferOutput are the types.
// The Valibot resolver
import { useForm } from "react-hook-form";
import { valibotResolver } from "@hookform/resolvers/valibot";
import * as v from "valibot";
// The Valibot schema with the pipe-based validation.
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"),
});
// The Valibot type inference.
type FormValues = v.InferOutput<typeof schema>;
function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
// The valibotResolver connects the schema to the form.
resolver: valibotResolver(schema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("username")} />
{errors.username && <p role="alert">{errors.username.message}</p>}
<input {...register("password")} type="password" />
{errors.password && <p role="alert">{errors.password.message}</p>}
<button type="submit">Sign up</button>
</form>
);
}
The valibotResolver(schema) is the mechanism that connects the Valibot schema to the form. The v.InferOutput<typeof schema> is the mechanism that derives the output type, and the v.InferInput is the input type . The Valibot supports both the firstError and the all criteria modes, and the criteriaMode: "all" is the option that the Valibot supports .
The Valibot’s v.transform is the mechanism that changes the output type, and the three generics are the requirement when the transform is used. The useForm<v.InferInput<typeof schema>, unknown, v.InferOutput<typeof schema>> is the mechanism that types the fields as the input and the submit result as the output . The transform is the reason the input and the output types differ, and the difference is the reason the three generics exist.
Complete Example Session
// ============================================
// PART 1: THE YUP RESOLVER
// ============================================
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const yupSchema = yup.object({
name: yup.string().required("Name is required"),
email: yup.string().email("Enter a valid email").required(),
});
type YupValues = yup.InferType<typeof yupSchema>;
function YupForm() {
const { register, handleSubmit, formState: { errors } } = useForm<YupValues>({
resolver: yupResolver(yupSchema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
<input {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</form>
);
}
// ============================================
// PART 2: THE JOI RESOLVER
// ============================================
import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";
const joiSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required(),
});
type JoiValues = { name: string; age: number };
function JoiForm() {
const { register, handleSubmit, formState: { errors } } = useForm<JoiValues>({
resolver: joiResolver(joiSchema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
<input type="number" {...register("age", { valueAsNumber: true })} />
{errors.age && <p>{errors.age.message}</p>}
</form>
);
}
// ============================================
// PART 3: THE SUPERSTRUCT RESOLVER
// ============================================
import { superstructResolver } from "@hookform/resolvers/superstruct";
import { object, string, number, Infer } from "superstruct";
const superstructSchema = object({
name: string(),
age: number(),
});
type SuperstructValues = Infer<typeof superstructSchema>;
function SuperstructForm() {
const { register, handleSubmit, formState: { errors } } = useForm<SuperstructValues>({
resolver: superstructResolver(superstructSchema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
<input type="number" {...register("age", { valueAsNumber: true })} />
{errors.age && <p>{errors.age.message}</p>}
</form>
);
}
// ============================================
// PART 4: THE VALIBOT RESOLVER
// ============================================
import { valibotResolver } from "@hookform/resolvers/valibot";
import * as v from "valibot";
const valibotSchema = v.object({
username: v.pipe(
v.string("username is required"),
v.minLength(3, "Needs to be at least 3 characters"),
),
password: v.string("password is required"),
});
type ValibotValues = v.InferOutput<typeof valibotSchema>;
function ValibotForm() {
const { register, handleSubmit, formState: { errors } } = useForm<ValibotValues>({
resolver: valibotResolver(valibotSchema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
<input {...register("password")} type="password" />
{errors.password && <p>{errors.password.message}</p>}
</form>
);
}
// ============================================
// PART 5: THE YUP CONTEXT
// ============================================
const contextSchema = yup.object({
email: yup.string().when("$plan", {
is: "paid",
then: (schema) => schema.email(),
otherwise: (schema) => schema,
}),
});
useForm<YupValues, { plan: "free" | "paid" }>({
resolver: yupResolver(contextSchema),
context: { plan: "paid" },
});
// ============================================
// PART 6: THE VALIBOT TRANSFORM
// ============================================
const transformSchema = v.object({
name: v.string(),
number: v.pipe(v.string(), v.transform(Number)),
});
// The transform changes the output type.
// The three generics type the input and the output.
useForm<
v.InferInput<typeof transformSchema>,
unknown,
v.InferOutput<typeof transformSchema>
>({
resolver: valibotResolver(transformSchema),
});
// ============================================
// PART 7: THE CRITERIA MODE
// ============================================
// The Yup, the Joi, and the Valibot support the criteriaMode: "all".
// The Superstruct supports only the firstError.
useForm({ criteriaMode: "all" });
// ============================================
// PART 8: THE INSTALLATION
// ============================================
// The Yup
// npm install react-hook-form @hookform/resolvers yup
// The Joi
// npm install react-hook-form @hookform/resolvers joi
// The Superstruct
// npm install react-hook-form @hookform/resolvers superstruct
// The Valibot
// npm install react-hook-form @hookform/resolvers valibot
// ============================================
// PART 9: THE RESOLVER COMPARISON
// ============================================
// The Yup: the type inference yes, the criteriaMode all.
// The Joi: the type inference no, the criteriaMode all.
// The Superstruct: the type inference yes, the criteriaMode firstError.
// The Valibot: the type inference yes, the criteriaMode all.
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The resolver package is installed.
// [ ] The schema library is installed.
// [ ] The resolver is the correct adapter.
// [ ] The type inference is used or the explicit generic is set.
// [ ] The criteriaMode is set if the multiple errors are needed.
// [ ] The valueAsNumber is set for the Joi and the Superstruct numeric fields.
The ten parts covered the Yup resolver, the Joi resolver, the Superstruct resolver, the Valibot resolver, the Yup context, the Valibot transform, the criteria mode, the installation, the resolver comparison, and the checklist. The through-line is that the adapter is the same, and the schema library is the choice.
Quick Reference
The resolver comparison
| Library | Resolver | Install | Infers values | criteriaMode |
|---|---|---|---|---|
| Yup | yupResolver | yup | Yes | firstError | all |
| Joi | joiResolver | joi | No | firstError | all |
| Superstruct | superstructResolver | superstruct | Yes | firstError |
| Valibot | valibotResolver | valibot | Yes | firstError | all |
The installation
| Library | Command |
|---|---|
| Yup | npm install react-hook-form @hookform/resolvers yup |
| Joi | npm install react-hook-form @hookform/resolvers joi |
| Superstruct | npm install react-hook-form @hookform/resolvers superstruct |
| Valibot | npm install react-hook-form @hookform/resolvers valibot |
The type inference
| Library | Utility |
|---|---|
| Yup | yup.InferType<typeof schema> |
| Joi | The manual type |
| Superstruct | Infer<typeof schema> |
| Valibot | v.InferOutput<typeof schema> |
The resolver options
| Option | Purpose |
|---|---|
mode: "async" | The async validation (default) |
mode: "sync" | The sync validation |
raw: true | The unparsed values |
Best Practices
โ Do This:
// Use the yupResolver for the Yup schema
useForm({ resolver: yupResolver(schema) }); // โ
// Use the yup.InferType for the type
type FormValues = yup.InferType<typeof schema>; // โ
// Use the valueAsNumber for the Joi numeric field
<input type="number" {...register("age", { valueAsNumber: true })} /> // โ
// Use the three generics for the Valibot transform
useForm<v.InferInput<typeof schema>, unknown, v.InferOutput<typeof schema>>(); // โ
โ Don’t Do This:
// Assume the Joi infers the type
useForm({ resolver: joiResolver(schema) }); // the type is any // โ
// Use the criteriaMode: "all" with the Superstruct
useForm({ criteriaMode: "all" }); // the Superstruct supports only firstError // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
The Joi type is any | The Joi does not infer | Set the explicit generic |
The Superstruct criteriaMode: "all" does nothing | The Superstruct supports only firstError | Use the default |
| The numeric field is a string | The valueAsNumber is missing | Add the valueAsNumber: true |
| The Valibot transform type is wrong | The three generics are not used | Use the InferInput and the InferOutput |
Real-World Examples
1. The Yup resolver
useForm({ resolver: yupResolver(schema) });
2. The Joi resolver
useForm({ resolver: joiResolver(schema) });
3. The Superstruct resolver
useForm({ resolver: superstructResolver(schema) });
4. The Valibot resolver
useForm({ resolver: valibotResolver(schema) });
5. The Yup type
type FormValues = yup.InferType<typeof schema>;
6. The Joi type
type FormValues = { name: string; age: number };
7. The Superstruct type
type FormValues = Infer<typeof schema>;
8. The Valibot type
type FormValues = v.InferOutput<typeof schema>;
9. The Yup context
useForm({ resolver: yupResolver(schema), context: { plan: "paid" } });
10. The Valibot transform
useForm<v.InferInput<typeof schema>, unknown, v.InferOutput<typeof schema>>();
Visual
The Adapter Pattern
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE ADAPTER PATTERN โ
โ โ
โ The schema library โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Yup / Joi / โ The validation rules โ
โ โ Superstruct / โ โ
โ โ Valibot โ โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ @hookform/resolvers โ The adapter โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ useForm โ The form โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Type Inference
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE TYPE INFERENCE โ
โ โ
โ โ
The Yup, the Superstruct, the Valibot: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The resolver infers โ The useForm without the generic โ
โ โ the type โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ The Joi: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The resolver does โ The explicit generic is required โ
โ โ not infer โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The criteriaMode
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE criteriaMode โ
โ โ
โ firstError (the default): โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The first failing โ The single message โ
โ โ rule โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ all: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Every failing rule โ The multiple messages โ
โ โ โ The Yup, the Joi, the Valibot โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| The adapter package | @hookform/resolvers |
| The Yup resolver | yupResolver from @hookform/resolvers/yup |
| The Joi resolver | joiResolver from @hookform/resolvers/joi |
| The Superstruct resolver | superstructResolver from @hookform/resolvers/superstruct |
| The Valibot resolver | valibotResolver from @hookform/resolvers/valibot |
| The type inference | The Yup, the Superstruct, the Valibot infer; the Joi does not |
| The criteriaMode | The Superstruct supports only firstError |
Key takeaways:
- The
@hookform/resolverspackage is the single adapter for the multiple libraries. The Yup, the Joi, the Superstruct, and the Valibot each have the adapter, and the adapter translates the library’s output to theformState.errorsshape . The form’s integration code is the same for all four. - The type inference differs. The Yup, the Superstruct, and the Valibot infer the form values type from the schema. The Joi does not, and the explicit generic is the requirement . The inference is the convenience, and the explicit generic is the fallback.
- The
criteriaModesupport differs. The Yup, the Joi, and the Valibot support both thefirstErrorand theall. The Superstruct supports only thefirstError. The difference is the mechanism that determines whether the UI can show the multiple messages. - The Yup context is the dynamic validation mechanism. The
useForm‘scontextoption is passed to the Yup schema, and the schema reads it through the$context . The context is the reason the Yup can do the conditional validation. - The Valibot transform requires the three generics. The
v.transformchanges the output type, and theuseForm<v.InferInput, unknown, v.InferOutput>is the mechanism that types the fields as the input and the submit result as the output . The transform is the reason the input and the output types differ.
Remember: The React Hook Form has the adapters for the multiple schema libraries, and the @hookform/resolvers package is the single bridge. The Yup, the Joi, the Superstruct, and the Valibot differ in the authoring style, the type inference, and the criteria mode, but the form’s integration is the same: the resolver: libraryResolver(schema). The adapter is the mechanism that makes the library choice independent of the form’s code.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!