There are many ways to kill a bird and as this saying implies, there are also many ways of handling forms in React/NextJS. There are also various libraries dedicated to helping us handle forms within React web applications and each of them has their own advantages and downsides. In this blog, I will share my go-to template when dealing with forms in NextJS that has proven to be most reliable.
One of the biggest challenges in handling forms in React (and by extension, Next.js) is balancing validation, UI consistency, and error handling in a way that scales without turning into spaghetti code. Over time, I’ve gravitated towards a combination that has proven both reliable and maintainable:
-
react-hook-form for form state management (minimal re-renders, easy integration).
-
zod for schema-based validation (type-safe and expressive).
-
@hookform/resolvers/zodResolver to plug Zod into React Hook Form.
-
Shadcn UI for consistent and accessible form components.
Getting Started
For this example, I am going to share my login form functionality using the above libraries. I always begin by defining my schema for the form structure using Zod. Here is my loginSchema.
Using Zod here ensures:
-
Validation logic is centralized.
-
TypeScript automatically infers the
LoginFormData
type. -
Transformations (like converting email to lowercase) happen before validation. This helps since I always save my emails as lowercase and db findunique is case sensitive although emails themselves are not.
After defining my form schema, I go ahead and set up the form using ShadCN elements and useForm hook. However, you can opt to not use ShadCN and just use your own custom form. One benefit of using ShadCN forms is that it automatically installs other depedencies, including zod, react-hook-form and the resolvers.
After installing the form from ShadCN, it is now time to hook up our form.
Here, I am using react-hook-form to handle the form event and submission, which ensures that the form always passes the validated data to the handlesubmit function. This ensures that users get instant validation as they type and therefore enhances safety.
As you can see in the handleSubmit
function, I am checking whether there is an error returned from my API and setting a custom error message to the associated field using form.setError
method. One small trick I use to improve UX is returning field-specific errors from the backend. For example, if the backend knows that the email is wrong, it returns:
Notice that I am always returning something even when there is an error. This is because NextJS does not allow throwing server errors on the client side and therefore throwing errors would be inappropriate. The fix is to always return something, and I am using success to track whether the response is successful.
Of course this code is not yet production ready and I have omitted many core functionalities that I use in production app, such as showing the loading states, tracking login attempts using user IP address and limit limiting attempts. I also use GoogleRecaptcha
to verify users and check whether there is a valid token before accepting the submission. I also use cookies to track the origin page so I can appropriately redirect the users to the page that they came from.
Let me know if you have any suggestions on how I could further improve my code. I have already shared on how to use React new useActionState
hook to achieve a similar design but with more focus on server-side validations.