- The Viable SaaS
- Posts
- How To Build Forms To Collect User Data In Your Next.js SaaS
How To Build Forms To Collect User Data In Your Next.js SaaS
Streamline form building and validation with react-hook-form and zod
While building a SaaS in Next.js, you’re likely going to have your users fill out a form of some sort. Whether it is something simple, such as signing up to use the app, or a lengthy medical questionnaire, you’re going to need to store and manipulate the form data.
Using React’s State Management
You could start with a simple HTML form with <input>
elements, set a value
and pass an onChange
event. Then you can manage that all with useState
. You would have code like:
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
...
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
required
placeholder="Email"
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
required
placeholder="Password"
/>
Using react-hook-form
Using React’s state for managing a form is cumbersome. You want to validate various inputs to a certain format, minimum length, maximum length, display errors, handle if they are required fields or not, etc.
Now your code might look like this:
import { useForm } from "react-hook-form";
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
getValues,
} = useForm();
...
<input
{...register("email", {
required: "Email is required",
})}
type="email"
placeholder="Email"
/>
{errors.email && (
<p>{`${errors.email.message}`}</p>
)}
<input
{...register("password", {
required: "Password is required",
minLength: {
value: 10,
message: "Password must be at least 10 characters",
},
})}
type="password"
placeholder="Password"
/>
{errors.password && (
<p>{`${errors.password.message}`}</p>
)}
See how fast things get dirty?
What if we could extract the validation, put it in a central place, and have it available for both the frontend and the backend to use?
Enter zod.
Zod allows you to declare a schema and validate values against it.
Using react-hook-form + zod
You probably have a file to store types. Add something like this to it:
import { z } from "zod";
export const registerUserSchema = z
.object({
email: z.string().email(),
password: z.string().min(8, "Password must be at least 8 characters"),
});
export type TRegisterUserSchema = z.infer<typeof registerUserSchema>;
And then you can import it into your form:
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { TRegisterUserSchema, registerUserSchema } from "@/lib/types";
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<TSignUpSchema>({
resolver: zodResolver(registerUserSchema),
});
<input
{...register("email")}
type="email"
placeholder="Email"
/>
{errors.email && (
<p>{`${errors.email.message}`}</p>
)}
<input
{...register("password")}
type="password"
placeholder="Password"
/>
{errors.password && (
<p>{`${errors.password.message}`}</p>
)}
Our form is back to being a lot cleaner and even has the benefit of being typed now. So if you change an input or miss a required one, your linter will let you know about it.
Using react-hook-form with zod will give you a performant form that doesn’t re-render randomly through the former and type safety through the latter.
This will make your forms much more predictable and easy for other developers to extend when they see them.
If you’re new to both, check out the documentation for react-hook-form and zod.
Reply