Day#14 Register Form with Validation in Next.js and React Hook Form

In this post, I will walk you through the process of building a registration form using Next.js and React Hook Form, complete with validation and dynamic language switching using i18next.

mariusz
mariusz
20 posts
2 followers

1. Creating the Registration Form

I will begin by building a simple form that accepts the following user details:

  • First Name
  • Last Name
  • Email
  • Password
  • Phone Number

I will use React Hook Form for easy form management and validation, and i18next for localization.

Here’s how you can set up the form (say page.tsx):

"use client";

import { useState } from "react";
import { useForm, SubmitHandler } from "react-hook-form"; // Importing useForm
import { useTranslation } from "next-i18next";
import InputField from "../../../components/InputField"; // Input component
import Button from "../../../components/Button"; // Button component

interface IFormInput {
  email: string;
  password: string;
  confirmPassword: string;
  phone: string;
  firstName: string; // Adding firstName field
  lastName: string;  // Adding lastName field
}

const RegisterForm = () => {
  const { t } = useTranslation("common"); // Load translations from the "common" namespace
  const { register, handleSubmit, formState: { errors }, watch } = useForm<IFormInput>(); // Using useForm

  const onSubmit: SubmitHandler<IFormInput> = async (data) => {
    console.log("Form submitted with data:", data); // Log submitted data

    // Example of sending data to backend (using fetch)
    try {
      const response = await fetch("localhost:3000/api/auth/register", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      });

      if (response.ok) {
        console.log("Registration successful!");
      } else {
        const errorData = await response.json();
        console.error("Registration failed:", errorData.message);
      }
    } catch (error) {
      console.error("Network error:", error);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <InputField
        label={t("first_name")}
        type="text"
        {...register("firstName", { 
          required: t("firstName_required"), 
          minLength: { value: 3, message: t("firstName_min_length") }
        })}
        error={errors.firstName?.message}
      />

      <InputField
        label={t("last_name")}
        type="text"
        {...register("lastName", { 
          required: t("lastName_required"), 
          minLength: { value: 3, message: t("lastName_min_length") }
        })}
        error={errors.lastName?.message}
      />

      <InputField
        label={t("email")}
        type="email"
        {...register("email", { required: t("email_required"), pattern: /^\S+@\S+\.\S+$/ })}
        error={errors.email?.message}
      />

      <InputField
        label={t("password")}
        type="password"
        {...register("password", { required: t("password_required"), minLength: 6 })}
        error={errors.password?.message}
      />

      <InputField
        label={t("confirm_password")}
        type="password"
        {...register("confirmPassword", {
          required: t("confirm_password_required"),
          validate: (value) => value === watch("password") || t("passwords_do_not_match"),
        })}
        error={errors.confirmPassword?.message}
      />

      <InputField
        label={t("phone_number")}
        type="tel"
        {...register("phone", { required: t("phone_required"), pattern: /^[0-9]{9}$/ })}
        error={errors.phone?.message}
      />

      <Button type="submit">
        {t("register")}
      </Button>
    </form>
  );
};

export default RegisterForm;

../../../components/Button.tsx

interface ButtonProps {
  type: "submit" | "button";
  children: React.ReactNode;
  disabled?: boolean;
  onClick?: () => void;
}

const Button = ({ type, children, disabled, onClick }: ButtonProps) => (
  <button
    type={type}
    onClick={onClick}
    disabled={disabled}
  >
    {children}
  </button>
);

export default Button;

../../../components/InputField.tsx

interface InputFieldProps {
  label: string;
  type: string;
  error?: string;
  [key: string]: any;
}

const InputField = ({ label, type, error, ...props }: InputFieldProps) => (
  <div>
    <label>{label}</label>
    <input
      type={type}
      {...props}
    />
    {error && <p>{error}</p>}
  </div>
);

export default InputField;

2. Handling Form Validation with React Hook Form

I also use React Hook Form for form validation. This library provides simple and flexible form validation. I validate the fields by using the register function from React Hook Form and set validation rules like required, minLength, and pattern.

Here is how I validate each field:

  • Email: It must be a valid email and required.
  • Password: It must be at least 6 characters long and required.
  • Confirm Password: It must match the password field and is required.
  • Phone Number: It must be a valid phone number, requiring only digits and a specific length.

3. Localization with i18next

In this implementation, I am using i18next for localization. This allows me to switch between languages dynamically. For example, I can easily switch the form between English and Polish by using a language toggle button.

3.1 Project Directory Structure for Localization

Here’s the directory structure for the localization files:

3.2 Configuration in next-i18next.config.js

I configure i18next in next-i18next.config.js:

// next-i18next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'pl'],  // Supported languages
    defaultLocale: 'en',     // Default language
  },
  react: { useSuspense: false }, // Disable Suspense for i18next (optional)
};

3.3 Translation Files

Here are the sample translation files for English and Polish.

en/common.json:

{
  "email": "Email",
  "password": "Password",
  "confirm_password": "Confirm Password",
  "phone_number": "Phone Number",
  "first_name": "First Name",
  "last_name": "Last Name",
  "email_required": "Email is required",
  "password_required": "Password is required",
  "firstName_required": "First name is required",
  "firstName_min_length": "First name must be at least 3 characters",
  "lastName_required": "Last name is required",
  "lastName_min_length": "Last name must be at least 3 characters",
  "register": "Register",
  "registering": "Registering..."
}

pl/common.json:

{
  "email": "Email",
  "password": "Hasło",
  "confirm_password": "Potwierdź Hasło",
  "phone_number": "Numer telefonu",
  "first_name": "Imię",
  "last_name": "Nazwisko",
  "email_required": "Email jest wymagany",
  "password_required": "Hasło jest wymagane",
  "firstName_required": "Imię jest wymagane",
  "firstName_min_length": "Imię musi mieć co najmniej 3 znaki",
  "lastName_required": "Nazwisko jest wymagane",
  "lastName_min_length": "Nazwisko musi mieć co najmniej 3 znaki",
  "register": "Zarejestruj się",
  "registering": "Rejestracja..."
}

3.4 Switching Languages Dynamically

To enable dynamic language switching, I added buttons in the RegisterForm to toggle between English and Polish. Here’s how you can do it:

// Inside the RegisterForm component
const toggleLanguage = (lang: string) => {
  i18next.changeLanguage(lang); // Change the language
};

return (
  <div>
    <button onClick={() => toggleLanguage("en")} className="px-2 py-1">
      en
    </button>
    /
    <button onClick={() => toggleLanguage("pl")} className="px-2 py-1">
      pl
    </button>
  </div>
);

This function will switch between English and Polish translations when the user clicks the buttons.

4. Common Issues with CORS and Routing

When working with Next.js in combination with an API on a different port, you may encounter CORS (Cross-Origin Resource Sharing) issues. This happens when your frontend (running on localhost:4000) tries to make requests to your backend (running on localhost:3000).

4.1 Solving CORS Issues

To solve the CORS issue, ensure that your backend is correctly configured to accept requests from the frontend’s origin. Here’s how you can set up CORS in your NestJS backend:

import * as cors from 'cors';

app.enableCors({
  origin: 'localhost:4000',  // Add your frontend URL here
  methods: 'GET,POST',
  allowedHeaders: 'Content-Type,Authorization',
});

This configuration will allow your backend to accept requests from localhost:4000.

4.2 Routing

If you’re using Next.js and NestJS, make sure the routes are correctly mapped. The frontend should make requests to the correct API endpoint (e.g., /api/auth/register), and you need to ensure that your proxy or API routing is correctly set up in Next.js to forward the request to your backend.


Summary

This is a simple registration form built with Next.js, React Hook Form, and i18next. I created form fields with validation and allowed dynamic language switching. If you want to improve or extend the form, feel free to adjust the validation rules or add new fields.

I hope this post was helpful! Feel free to let me know what you think on Mastodon.


Opublikowano

w

,

przez

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *