Skip to content

Validation Guide

@enlolab/forms leverages the Zod schema library and the validation capabilities of react-hook-form to offer concise yet powerful input validation. This guide explains how validation works out-of-the-box and how you can customise it to fit complex scenarios.

TL;DR – Define a schema for each field; @enlolab/forms automatically wires it to react-hook-form and surfaces errors next to the corresponding input. No additional code is needed.

The simplest way to add validation is to assign a schema to your field configuration:

import { z } from "zod";
fields: {
email: {
type: "email",
label: "Email",
placeholder: "you@example.com",
schema: z.string().email("Invalid email address")
}
}

@enlolab/forms extracts those schemas, builds a single Zod object, and passes it to react-hook-form’s resolver. The error message defined in the schema ("Invalid email address") is rendered automatically via <FormMessage />.

Use Zod helpers just as you would outside @enlolab/forms:

schema: z.string().min(1, "This field is required");

If you need an optional field:

schema: z.string().optional();

Multi-step forms validate only the fields present in the current step. Internally the hook useFormValidation triggers validation for steps[currentStepId].fields. If validation passes, nextStep() is called; otherwise @enlolab/forms:

  1. Focuses the first invalid field.
  2. Displays an error toast (sonner) – configurable in upcoming versions.
  3. Prevents navigation until all errors are resolved.

If a step contains no fields the validation helper returns true immediately, enabling you to create purely informational steps.

Zod’s refine, superRefine, or composing schemas lets you express relationships between fields:

import { z } from "zod";
const passwordSchema = z
.string()
.min(8, "Password must be at least 8 characters");
fields: {
password: {
type: "text",
label: "Password",
schema: passwordSchema
},
confirmPassword: {
type: "text",
label: "Confirm Password",
schema: z.string()
.refine((val, ctx) => val === ctx.parent.password, {
message: "Passwords do not match"
})
}
}

Because the resolver holds the full form values, you can inspect ctx.parent or use z.object composition to compare multiple fields.

By default validation runs on onChange and onBlur events (React Hook Form’s mode = "onChange"). To change the strategy wrap the component and override RHF options:

const MyForm = FormFactory(config);
export const Wrapped = () => {
return <MyForm methodsOptions={{ mode: "onSubmit" }} />;
};

Note: Passing methodsOptions is on the roadmap – for now fork useFormState if you need full control.

@enlolab/forms exposes the raw methods object from react-hook-form through callbacks (onError, etc.). You can therefore implement any error UX you like:

onError: (error, values) => {
sendErrorToSentry(error, values);
};

Or override styling via the error_className prop on a per-field basis.

  1. Be explicit – Always define a schema; implicit any values bypass validation.
  2. Reuse schemas – Extract common rules into helpers to avoid duplication.
  3. Provide actionable messages – Users should understand how to fix errors.
  4. Validate progressively – Multi-step validation improves UX and performance.