How I implemented Zod and React Hook Form for ‘Register’ Component And Lessons I’ve Learned

Mai Vang
5 min readJun 14, 2024

--

Successfully registered

Here’s what I recently finished at #GIS: I focused on ensuring ‘Register’ and ‘Login’ components were properly wrapped around in <form>tags, enabling users to press ‘Enter’ instead of scrolling their mouse to click on the button. Please see ticket assigned to me here.

Why use Zod and React Hook Form?

Zod: A TypeScript-first schema declaration and validation library, perfect for defining and validating form data.

React Hook Form: A performant, flexible, and extensible form library for React that makes managing forms and validation straightforward.

Here’s the GridIron Survivor (GIS) project for you to follow along: https://github.com/LetsGetTechnical/gridiron-survivor. I will be discussing ‘Register’ to keep this documentation short.

Setting up the Project

React Hook Form, Zod, and Hookform/Resolvers packages have been installed in the project already.

What the Registration Form Previously Looked Like:

‘Register’ component can be found here using this path (app/register/page.tsx)

'use client';
import { useState, ChangeEvent, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { Models } from 'appwrite/types/models';

import { Input } from '@/components/Input/Input';
import Logo from '@/components/Logo/Logo';
import { Button } from '@/components/Button/Button';
import { registerAccount } from '@/api/apiFunctions';

import logo from '/public/assets/logo-colored-outline.svg';

import { useAuthContext } from '@/context/AuthContextProvider';

const Register = () => {
const router = useRouter();
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>('');
const { loginAccount, isSignedIn } = useAuthContext();

const handleEmail = (event: ChangeEvent<HTMLInputElement>): void => {
setEmail(event.target.value);
};

const handlePassword = (event: ChangeEvent<HTMLInputElement>): void => {
setPassword(event.target.value);
};

const comparePasswords = (password: string, confirmPassword: string) => {
return password === confirmPassword;
};

const handleConfirmPassword = (
event: ChangeEvent<HTMLInputElement>,
): void => {
setConfirmPassword(event.target.value);
};

const handleDisabled = () => {
const passwordsMatch = comparePasswords(password, confirmPassword);
if (email && passwordsMatch === true && confirmPassword !== '') {
return false;
}
return true;
};

const handleRegister = async () => {
try {
const accountRegistered = await registerAccount({ email, password });
console.log(accountRegistered.$id, accountRegistered.email);
await loginAccount({ email, password });
router.push('/weeklyPicks');
} catch (error) {
console.error('Registration Failed', error);
}
};

useEffect(() => {
if (isSignedIn) {
router.push('/weeklyPicks');
}
}, [isSignedIn, router]);

return (
<div className="h-screen w-full">
<div className="grid h-screen w-full grid-cols-2 bg-gradient-to-b from-[#4E160E] to-zinc-950">
<div className="grid p-8">
<div className="grid">
<Logo className="mx-auto place-self-end" src={logo} />
</div>
<div className="mx-auto grid gap-4 place-self-end">
<p className="leading-7 text-white">
Thank you... fantasy football draft, for letting me know that even
in my fantasies, I am bad at sports.
</p>
<p className="leading-7 text-white">Jimmy Fallon</p>
</div>
</div>
<div className="grid place-content-center bg-white p-8">
<div className="mx-auto grid w-80 place-content-center gap-4">
<h1 className="text-5xl font-extrabold tracking-tight text-foreground">
Register A New Account
</h1>
<p className="pb-4 font-normal leading-7 text-zinc-500">
If you have an existing account{' '}
<Link href="/login" className="hover:text-orange-600">
Login!
</Link>
</p>
<Input
data-testid="email"
type="email"
value={email}
placeholder="Email"
onChange={handleEmail}
/>
<Input
data-testid="password"
type="password"
value={password}
placeholder="Password"
onChange={handlePassword}
/>
<Input
data-testid="confirm-password"
type="password"
value={confirmPassword}
placeholder="Confirm Password"
onChange={handleConfirmPassword}
/>
<Button
data-testid="continue-button"
label="Register"
disabled={handleDisabled()}
onClick={handleRegister}
/>
<Link href="/login" className="hover:text-orange-600">
Login to get started playing
</Link>
</div>
</div>
</div>
</div>
);
};

export default Register;

While this code is working correctly as intended, this can be made more accessible and manageable using a <form> tage and a structured validation with Zod and React Hook Form.

Now, let’s build and update it.

Building the Registration Form

Step 1: Define a schema to validate form fields with Zod.
The following fields are email, password, and confirmPassword since the inputs in the fields are going to be used.

The .refine is used to ensure password and confirmPassword match before they can proceed further or else user will receive an error.

const RegisterUserSchema = z
.object({
email: z
.string()
.min(1, { message: 'Please enter an email address' })
.email({ message: 'Please enter a valid email address' }),
password: z
.string()
.min(1, { message: 'Please enter a password' })
.min(6, { message: 'Password must be at least 6 characters' }),
confirmPassword: z
.string()
.min(1, { message: 'Please confirm your password' })
.min(6, { message: 'Password must be at least 6 characters' }),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});

type RegisterUserSchemaType = z.infer<typeof RegisterUserSchema>;

Step 2: Setup React Hook Form
Use the useForm hook from react-hook-form and pass the Zod Schema resolver

const form = useForm<RegisterUserSchemaType>({
resolver: zodResolver(RegisterUserSchema),
});

Step 3: Watch Form Fields
Since one of our functionalities is to ensure the ‘continue’ button is disabled, use useWatch to keep track of form values.


const email = useWatch({
control: form.control,
name: 'email',
defaultValue: '',
});


const password = useWatch({
control: form.control,
name: 'password',
defaultValue: '',
});

const confirmPassword = useWatch({
control: form.control,
name: 'confirmPassword',
defaultValue: '',
});

Step 4: Implement Form Submission
Define a Submit Handler — create an onSubmit function to handle form submission:

  const onSubmit: SubmitHandler<RegisterUserSchemaType> = async (data) => {
try {
await registerAccount(data);
await loginAccount(data);
router.push('/weeklyPicks');
} catch (error) {
console.error('Registration Failed', error);
}
};

Step 5: Create the isDisabled functionality
create a isDisabled function to not allow users to click on button to continue

  const isDisabled = !email || !password || password !== confirmPassword;

Step 6: Form Component
Wrap the form elements with Form from ShadCN components and add onSubmitto the <form> tag

 <form
id="input-container"
className="grid gap-3"
onSubmit={form.handleSubmit(onSubmit)}
>

Step 7: Implement Form Fields
Use FormField, FormItem, FormControl, and FormMessage to set up the form fields that was created previously from ShadCN.

 <Form {...form}>
<form
id="input-container"
className="grid gap-3"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control as Control<object>}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
data-testid="email"
type="email"
placeholder="Email"
{...field}
/>
</FormControl>
{form.formState.errors.email && (
<FormMessage>
{form.formState.errors.email.message}
</FormMessage>
)}
</FormItem>
)}
/>
<FormField
control={form.control as Control<object>}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
data-testid="password"
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
{form.formState.errors.password && (
<FormMessage>
{form.formState.errors.password.message}
</FormMessage>
)}
</FormItem>
)}
/>
<FormField
control={form.control as Control<object>}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
data-testid="confirm-password"
type="password"
placeholder="Confirm Password"
{...field}
/>
</FormControl>
{form.formState.errors.confirmPassword && (
<FormMessage>
{form.formState.errors.confirmPassword.message}
</FormMessage>
)}
</FormItem>
)}
/>

Step 8: Add the disabled function to the button


<Button
data-testid="continue-button"
label="Register"
type="submit"
disabled={isDisabled}
/>

To sum it all up:

  1. I defined a Zod schema for form validation and added a method to ensure passwords match accordingly (refine).
  2. I initialized a React Hook Form with the Zod schema resolver.
  3. I used useWatch to monitor inputs from form fields
  4. Then implemented onSubmit to handle form submission.
  5. Lastly, implemented the disabled button to control the needed inputs.

Lessons I’ve learned along the way:

I learned how to set up a Zod Schema, implementing React Hook Form methods, using Shad CN components, and got more familiar with mock testing ‘register’ and not actually calling the API, espeically given Appwrite’s rate limits.

Please let me know if you would have done it any other way :)

--

--

Mai Vang

Analytical and excited software engineer experienced. Check out more here: https://mai-vang-swe.vercel.app/