Skip to content

Schema Validation

Rapid Form’s resolver API lets you plug in any schema validation library — Zod, Yup, or a custom validator — without any runtime dependency on those libraries inside rapid-form itself.

Pass a resolver function to refValidation. It receives all current form values as a Record<string, string> and returns a map of field names to error messages. Return undefined (or omit the key) for fields that pass.

type SchemaResolver = (
values: Record<string, string>
) =>
| Record<string, string | undefined>
| Promise<Record<string, string | undefined>>

The resolver runs on every field event and on form submit. When resolver is present, it replaces per-field validations.


rapid-form ships two ready-made adapters, each imported from its own sub-path so they never increase the main bundle size.

Terminal window
npm install zod
import { z } from 'zod';
import { useRapidForm } from 'rapid-form';
import { zodResolver } from 'rapid-form/resolvers/zod';
const schema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(3, 'At least 3 characters'),
});
export function SignUpForm() {
const { refValidation, errors } = useRapidForm();
return (
<form ref={(ref) => refValidation(ref, { resolver: zodResolver(schema) })}>
<input type="email" name="email" />
{errors['email']?.isInvalid && <span>{errors['email'].message}</span>}
<input type="text" name="name" />
{errors['name']?.isInvalid && <span>{errors['name'].message}</span>}
<button type="submit">Sign up</button>
</form>
);
}
Terminal window
npm install yup
import * as yup from 'yup';
import { useRapidForm } from 'rapid-form';
import { yupResolver } from 'rapid-form/resolvers/yup';
const schema = yup.object({
email: yup.string().email('Invalid email address').required('Required'),
name: yup.string().min(3, 'At least 3 characters').required('Required'),
});
export function SignUpForm() {
const { refValidation, errors } = useRapidForm();
return (
<form ref={(ref) => refValidation(ref, { resolver: yupResolver(schema) })}>
<input type="email" name="email" />
{errors['email']?.isInvalid && <span>{errors['email'].message}</span>}
<input type="text" name="name" />
{errors['name']?.isInvalid && <span>{errors['name'].message}</span>}
<button type="submit">Sign up</button>
</form>
);
}

If you use a different library or want full control, write a SchemaResolver directly:

import type { SchemaResolver } from 'rapid-form';
const myResolver: SchemaResolver = ({ email, name }) => ({
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? undefined : 'Invalid email',
name: name.length >= 3 ? undefined : 'At least 3 characters',
});
// Use it the same way:
refValidation(ref, { resolver: myResolver });

Resolvers can also be async — useful for remote checks like username availability:

const asyncResolver: SchemaResolver = async ({ username }) => {
const taken = await fetch(`/api/check-username?u=${username}`).then(r => r.json());
return { username: taken ? 'Username already taken' : undefined };
};

The resolver respects resetOnSubmit (defaults to true). The form resets only when the resolver returns no errors:

refValidation(ref, {
resolver: zodResolver(schema),
resetOnSubmit: false, // keep values after a valid submit
})

ScenarioResult
Field event firesResolver runs with all current values; errors update immediately
Submit with errorse.preventDefault() called; errors shown; form not reset
Submit with no errorsForm resets (if resetOnSubmit: true)
resolver + validationsresolver takes precedence; validations is ignored