There are some issues with your code. As the comment above said, the useForm is initialized once and as such, the conditional logic in the resolver and defaultValues won't do anything when the step changes. Here is what you need to do to make it work.
Here is the full code:
import { z } from "zod";
import { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const formSchema = z.object({
dateOfBirth: z.string().min(1, "Date of birth is required"),
phoneNumber: z.string().min(5, "Phone number is required"),
streetAddress: z.string().min(1, "Street address is required"),
city: z.string().min(1, "City is required"),
emergencyContactName: z.string().min(1, "Name is required"),
emergencyContactPhone: z.string().min(5, "Phone number is required"),
emergencyContactRelation: z.string().min(1, "Relation is required"),
experienceLevel: z.enum(["none", "beginner", "intermediate", "advanced"], {
errorMap: () => ({ message: "Experience Level is required" }),
}),
experienceDescription: z.string().optional(),
hasMedicalCondition: z.enum(["yes", "no"], {
errorMap: () => ({ message: "please select yes or no" }),
}),
consent: z.enum(["true"], {
errorMap: () => ({ message: "You must agree to continue" }),
}),
});
const OnboardPage = () => {
const [step, setStep] = useState<1 | 2>(1);
const {
register,
control,
handleSubmit,
formState: { errors },
trigger,
} = useForm({
resolver: zodResolver(formSchema),
mode: "onChange",
defaultValues: {
dateOfBirth: "",
phoneNumber: "",
streetAddress: "",
city: "",
emergencyContactName: "",
emergencyContactPhone: "",
emergencyContactRelation: "",
experienceLevel: undefined,
experienceDescription: "",
hasMedicalCondition: "" as z.infer<
typeof formSchema.shape.hasMedicalCondition
>,
consent: "" as z.infer<typeof formSchema.shape.consent>,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log("Form Data:", data);
};
const handlePrevious = () => {
if (step === 2) {
setStep(1);
}
};
const handleNext = async () => {
const fieldsToValidate =
step === 1
? ([
"dateOfBirth",
"phoneNumber",
"streetAddress",
"city",
"emergencyContactName",
"emergencyContactPhone",
"emergencyContactRelation",
"experienceLevel",
"experienceDescription",
] as const)
: (["hasMedicalCondition", "consent"] as const);
const isValid = await trigger(fieldsToValidate);
if (isValid) setStep(2);
if (step === 2) {
handleSubmit(onSubmit)();
}
};
return (
<div>
<h1>Onboard</h1>
<form>
{step === 1 && (
<>
<div>
<label>Date of Birth</label>
<input {...register("dateOfBirth")} />
{errors.dateOfBirth && (
<p className="error-messsage">{errors.dateOfBirth.message}</p>
)}
</div>
<div>
<label>Phone Number</label>
<input {...register("phoneNumber")} />
{errors.phoneNumber && (
<p className="error-messsage">{errors.phoneNumber.message}</p>
)}
</div>
<div>
<label>Street Address</label>
<input {...register("streetAddress")} />
{errors.streetAddress && (
<p className="error-messsage">{errors.streetAddress.message}</p>
)}
</div>
<div>
<label>City</label>
<input {...register("city")} />
{errors.city && (
<p className="error-messsage">{errors.city.message}</p>
)}
</div>
<div>
<label>Emergency Contact Name</label>
<input {...register("emergencyContactName")} />
{errors.emergencyContactName && (
<p className="error-messsage">
{errors.emergencyContactName.message}
</p>
)}
</div>
<div>
<label>Emergency Contact Phone</label>
<input {...register("emergencyContactPhone")} />
{errors.emergencyContactPhone && (
<p className="error-messsage">
{errors.emergencyContactPhone.message}
</p>
)}
</div>
<div>
<label>Emergency Contact Relation</label>
<input {...register("emergencyContactRelation")} />
{errors.emergencyContactRelation && (
<p className="error-messsage">
{errors.emergencyContactRelation.message}
</p>
)}
</div>
<div>
<label>Experience Level</label>
<select {...register("experienceLevel")}>
<option value="">Select</option>
<option value="none">None</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
{errors.experienceLevel && (
<p className="error-messsage">
{errors.experienceLevel.message}
</p>
)}
</div>
<div>
<label>Experience Description</label>
<textarea {...register("experienceDescription")} />
{errors.experienceDescription && (
<p className="error-messsage">
{errors.experienceDescription.message}
</p>
)}
</div>
</>
)}
{step === 2 && (
<>
<div>
<label>Do you have a medical condition?</label>
<select {...register("hasMedicalCondition")}>
<option value="">Select</option>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
{errors.hasMedicalCondition && (
<p className="error-messsage">
{errors.hasMedicalCondition.message}
</p>
)}
</div>
<div>
<label>
<Controller
name="consent"
control={control}
render={({ field }) => (
<input
type="checkbox"
{...field}
onChange={(e) =>
field.onChange(e.target.checked ? "true" : "")
}
checked={field.value === "true"}
/>
)}
/>
I agree to the terms and conditions
</label>
{errors.consent && (
<p className="error-messsage">{errors.consent.message}</p>
)}
</div>
</>
)}
{step === 2 && (
<button type="button" onClick={handlePrevious}>
Previous
</button>
)}
<button type="button" onClick={handleNext}>
{step === 1 ? "Next" : "Submit"}
</button>
</form>
</div>
);
};
export default OnboardPage;
.error-messsage {
font-size: 12px;
color: red;
}