Beyond the Basics: Building Scalable Forms with Reusable Custom Hooks for React/Next

Oladipupo Ishola
5 min readSep 2, 2024

--

I wrote an article about form validation using React Hook Form and Zod some time ago, you can see the link below.

However, this approach works fine with validating one or two forms considering that your application is minimal. However, when the application is bigger and you have more forms to validate, you’ll have to manually import the React Hook Form package everywhere you need to perform a form validation. There is absolutely nothing wrong with this approach, however, we will be repeating codes everywhere we need to perform input or form validations, which is against the Don’t Repeat Yourself (DRY) programming principle.

This article serves as an update of the first, please know that the demo implementation in the first article did not repeat any code of a sort since I only validated a single form but using the approach will lead to importing of RHF components everywhere it is required in a larger app, peradventure you are building a pet project feel free to discard my opinion here.

Note: This article is not solely about the basics of React Hooks but about its usefulness, and its purpose of creation, which is its ability to share stateful logic or behavior and not the state itself among components, which helps eliminate code repetitions. In case you are new to React Hooks, kindly check these articles Hooks in a Glance and Why React Hooks

You can also check my articles on some of the custom hooks that you can start using in your projects.

What are we trying to solve?

We are trying to avoid importing the RHF components and the like anywhere we need to perform form validations.

We’ll create a custom hook that imports the React Hook Form components, the Zod resolver, and their types. The hook takes in the parameter of the schema since we want the hook to be able to validate the schema, and since we don’t have an idea what the shape of the schema will look like, the form data extends the record and the schema takes in the type of ZodType.

The hook then returns an object of React Hook Form components to perform all sorts of form registration.

Create a folder called hooks, and inside of it, you can name the hook whatever you like, and make sure the hook name starts with useSomething to follow the React Hooks naming convention.

import { useForm } from "react-hook-form";
import { ZodType } from "zod";
import { zodResolver } from '@hookform/resolvers/zod';

export const useFormWithValidation = <T extends Record<string, any>>(
validationSchema: ZodType<T>
) => {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<T>({
resolver: zodResolver(validationSchema),
});


return { register, handleSubmit, errors, reset, isSubmitting };
};

Time to take the custom hook for a spin. We will be reusing the previous schema from the last article, creating a new form component, and then importing the custom hook and passing in the schema as an argument.

export const schema = z
.object({
username: z
.string()
.min(3, { message: "Username must be at least 3 characters" }),
email: z
.string()
.min(1, { message: "Email is required" })
.email("Invalid email address"),
password: z
.string()
.min(6, { message: "Password must be at least 6 characters" }),
confirmPassword: z
.string()
.min(6, { message: "Password must be at least 6 characters" }),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords does not match",
});

export type ValidationSchemaType = z.infer<typeof schema>

Inside the NewForm component, we imported the schema and the useFormWithValidation and registered the form component as normal.

The onSubmit function performs the sending of the form data to the backend API and reset the form inputs. Since there is no backend API, I only mimic making an API request and then console.log the data.

"use client";

import React from "react";
import styles from "./form.module.scss";
import { schema, ValidationSchemaType } from "@/utils/schema";
import { useFormWithValidation } from "@/hooks/useFormHook";

export default function NewForm() {
const { register, handleSubmit, errors, isSubmitting } = useFormWithValidation(schema);

const onSubmit = async (data: ValidationSchemaType) => {
try {
// simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log(data);
reset();
} catch (error) {
console.error("Error submitting form:", error);
}
};

return (
<form className={styles.form_main} onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="username">
Username:
<input
type="text"
placeholder="username goes here..."
{...register("username")}
className={errors.username && styles.error_input}
/>
{errors.username && (
<span className={styles.error}>{errors.username?.message}</span>
)}
</label>
<label htmlFor="email">
Email:
<input
type="email"
placeholder="email goes here..."
{...register("email")}
className={errors.email && styles.error_input}
/>
{errors.email && (
<span className={styles.error}>{errors.email?.message}</span>
)}
</label>
<label htmlFor="password">
Password:
<input
type="password"
placeholder="password goes here..."
{...register("password")}
className={errors.password && styles.error_input}
/>
{errors.password && (
<span className={styles.error}>{errors.password?.message}</span>
)}
</label>
<label htmlFor="confirmPassword">
Confirm Password:
<input
type="password"
placeholder="Confirm password"
{...register("confirmPassword")}
className={errors.confirmPassword && styles.error_input}
/>
{errors.confirmPassword && (
<span className={styles.error}>
{errors.confirmPassword?.message}
</span>
)}
</label>
<button type='submit' disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}

And there you have it, cleaner and shorter, and the good thing is, the hook can be further customized and reused in any of your future projects.

we did it

Thank you for reading!

The demo’s GitHub repository is available here, where you can view the entire code as well as the CSS stylings.
Happy coding!

And if you haven’t already subscribed to me, you can just click here to Subscribe to my newsletter ❤️ and be the first to read my newly published articles.

And finally, if you’d like to support my writing journey, you can help me keep going by buying me a coffee. Buy a coffee

👇 Do you know you can Clap 50 times 🙃 . Let us see how much you Clap for me. It matters to me.❤️ 👇

--

--

Oladipupo Ishola
Oladipupo Ishola

Written by Oladipupo Ishola

Tech Writer | Full Stack Developer | Frontend Developer | Backend Developer | Co-Building @techverseacademy | Mentor & Code Instructor | MLH ’21 Alum

Responses (1)