When creating forms in a React application, the code complexity is based on the form size. In a previous article, we used a custom form creation method to show a React contact form. It contains several states and handlers exclusively created and managed for the form fields.
If the form is high complex with more number of fields, we have to look for a better alternative to the custom form creation.
React Hook Form is a very good option to work with web forms. This library supports hooks for handling input-change and form-submit events. It also provides built-in properties for form validation.
This tutorial is to learn how to use React hook form to develop forms. We used the features of the React hook form library with its useful hooks and properties.
It also closes the loop by covering the backend api in PHP to process and manage the form inputs.
This example has a complete workflow for athletes registration using React Hook Form on the frontend and PHP + MySQL on the backend. It highlights the React Hook Form library hooks for form validation and submission.
It collects basic athlete information using a simple, user-friendly form rendered by a React JSX layout. It uses the useForm hook to register form state, input and the validation rules.
It imports Axios to communicate with the backend API. We have seen several React examples that connect to the API via axios to request server resources. For example, in a React file upload script, we posted the file binary via axios.
Part of (UserRegisterform.jsx) – Form layout
import React from "react";
import { useForm } from "react-hook-form";
import { validationRules } from "./Validation-rules";
import { requiredFieldValidation } from "./Validation-rules";
import "./UserRegisterForm.css";
const User_register_form = () => {
const { register, handleSubmit, formState: { errors }, reset, setError } = useForm();
...
...
return (
<div className="user-register-container">
<div className="user-register-card">
<div className="user-register-form-section">
<div style={{ marginBottom: "24px", width: "425px" }}>
<h1 className="user-register-title">Athlete Registration Form</h1>
<p className="user-register-subtitle">
Thank you for signing up at our club.
<br />
Please fill in your information and we will contact you shortly to
complete your membership.
</p>
</div>
{/* Banner */}
{banner.message && (
<div className={`response-message ${banner.type}`}>
{banner.message}
</div>
)}
{/* Form */}
<form onSubmit={handleSubmit(onSubmit)} className="user-register-form">
{/* First Name */}
<div>
<label className="form-label">First Name:</label>
<input
type="text"
{...register("firstName", {
required: "First name is required",
...validationRules.firstName,
})}
className="form-input"
/>
{errors.firstName && <p className="user-register-error">{errors.firstName.message}</p>}
</div>
{/* Last Name */}
<div>
<label className="form-label">Last Name:</label>
<input
type="text"
{...register("lastName", {
required: "Last name is required",
...validationRules.lastName,
})}
className="form-input"
/>
{errors.lastName && <p className="user-register-error">{errors.lastName.message}</p>}
</div>
{/* Email */}
<div>
<label className="form-label">Email:</label>
<input
type="email"
{...register("email", {
required: "Email is required",
...validationRules.email,
})}
className="form-input"
/>
{errors.email && <p className="user-register-error">{errors.email.message}</p>}
</div>
{/* Phone */}
<div>
<label className="form-label">Phone:</label>
<input
type="tel"
{...register("phone", {
required: "Phone is required",
...validationRules.phone,
})}
className="form-input"
/>
{errors.phone && <p className="user-register-error">{errors.phone.message}</p>}
</div>
<div>
<label className="form-label">Gender:</label>
<div style={{ display: "flex", gap: "24px" }}>
<label>
<input
type="radio"
value="male"
{...register("gender", requiredFieldValidation)}
/> Male
</label>
<label>
<input
type="radio"
value="female"
{...register("gender", requiredFieldValidation)}
/> Female
</label>
</div>
{errors.gender && (
<p className="user-register-error">{errors.gender.message}</p>
)}
</div>
{/* Weight */}
<div>
<label className="form-label">Weight (kg):</label>
<input
type="number"
{...register("weight", {
required: "Weight is required",
...validationRules.weight,
})}
className="form-input"
/>
{errors.weight && <p className="user-register-error">{errors.weight.message}</p>}
</div>
{/* Height */}
<div>
<label className="form-label">Height (cm):</label>
<input
type="number"
{...register("height", {
required: "Height is required",
...validationRules.height,
})}
className="form-input"
/>
{errors.height && <p className="user-register-error">{errors.height.message}</p>}
</div>
<div>
<label className="form-label">
Please describe the history of chronic illness:
</label>
<textarea
{...register("chronicIllness")}
className="form-input user-register-textarea"
placeholder="Please describe any chronic illnesses..."
/>
</div>
<div>
<label className="form-label">Does the athlete have allergies?</label>
<div style={{ display: "flex", gap: "24px" }}>
<label>
<input
type="radio"
value="yes"
{...register("allergies", requiredFieldValidation)}
/> Yes
</label>
<label>
<input
type="radio"
value="no"
{...register("allergies", requiredFieldValidation)}
/> No
</label>
</div>
{errors.allergies && (
<p className="user-register-error">{errors.allergies.message}</p>
)}
</div>
<button type="submit" className="user-register-button">
REGISTER
</button>
</form>
</div>
</div>
</div>
);
};
export default User_register_form;
In the above section, each input registers the validation rules’ property defined for the fields. The rules are defined for the name, email, and phone fields in a separate JavaScript file as shown in the below code.
This JS is imported into the JSX, where the form fields are registered with React hook form. The email and phone fields are going through a pattern validation based on regular expression.
validation-rules.js
// validationRules.js
export const validationRules = {
firstName: {
minLength: { value: 2, message: "Must have at least 2 characters" },
},
lastName: {
minLength: { value: 2, message: "Must have at least 2 characters" },
},
email: {
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Must match a valid email format (abc@domain.com)",
},
},
phone: {
pattern: {
value: /^[\d\s\-+()]{10,}$/,
message:
"Must be at least 10 characters and only digits, spaces, -, +, (, ) allowed",
},
},
weight: {
min: { value: 20, message: "Weight must be at least 20kg" },
max: { value: 300, message: "Weight cannot exceed 300kg" },
},
height: {
min: { value: 100, message: "Height must be at least 100cm" },
max: { value: 250, message: "Height cannot exceed 250cm" },
},
};
export const requiredFieldValidation = { required: "required" };
React Hook form’s useForm provide a built-in object for handleSubmit. It receives the JS callback to be called on form submit.
This example uses Axios to make a post request to an api endpoint. It prepares the UI element to show the response text in a ribbon like banner.
It handles the types of errors and their messages to update and render the response banner.
Part of (UserRegisterForm.jsx) with state definition and handleSubmit
import React from "react";
import axios from "axios";
import { validationRules } from "./Validation-rules";
import { requiredFieldValidation } from "./Validation-rules";
import "./UserRegisterForm.css";
const User_register_form = () => {
const { register, handleSubmit, formState: { errors }, reset, setError } = useForm();
const [banner, setBanner] = React.useState({ message: "", type: "" });
const onSubmit = async (data) => {
try {
const response = await axios.post(
"http://localhost/react-hook-form-action/register-action-ajax.php",
new URLSearchParams(data),
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
);
const result = response.data;
if (result.status === "error") {
setError("email", { type: "manual", message: result.message });
setBanner({ message: result.message, type: "error" });
} else {
setBanner({ message: result.message, type: "success" });
reset();
}
} catch (error) {
console.error("Error:", error);
setBanner({ message: "Something went wrong.", type: "error" });
}
};
This is usual PHP file which processes the form data posted via axios. This PHP API endpoint allows the origin where the React app is running.
Path to your api/react-hook-form-action/register-action-ajax.php
<?php
header("Content-Type: application/json");
header("Access-Control-Allow-Origin: http://localhost:5173");
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if(empty($_POST)) {
echo json_encode(["status" => "error", "message" => "No data received"]);
exit;
}
include __DIR__ . '/db.php';
// Collect data
$firstName = $_POST['firstName'] ?? '';
$lastName = $_POST['lastName'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$gender = $_POST['gender'] ?? '';
$weight = $_POST['weight'] ?? '';
$height = $_POST['height'] ?? '';
$chronicIllness = $_POST['chronicIllness'] ?? '';
$allergies = $_POST['allergies'] ?? '';
// Check if email already exists
$sql = "SELECT id FROM tbl_user WHERE email = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
echo json_encode(["status" => "error", "message" => "Email already exists"]);
exit;
}
// Insert new record
$sql = "INSERT INTO tbl_user (first_name, last_name, email, phone, gender, weight, height, chronic_illness, allergies, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";
$stmt = $conn->prepare($sql);
$stmt->bind_param("sssssdsss", $firstName, $lastName, $email, $phone, $gender, $weight, $height, $chronicIllness, $allergies);
if ($stmt->execute()) {
echo json_encode(["status" => "success", "message" => "Thank you for registering!"]);
} else {
echo json_encode(["status" => "error", "message" => "Database error: " . $conn->error]);
}
In the above PHP script, it validates that the posted email is already registered. It compares the database entries and confirms the uniqueness of the email address.
If an email already exists, then the registration process will be stopped. The API will respond with a message alert in JSON format.
On the React end, it parses the JSON object and updates the layout to re-render the banner with the “Email already exists” message.
Once the form data is stored successfully, the API will respond with a “Thank you” message for the React layout.
Download the React hook form example code and unzip it into your environment. After that, run the npm install
to have all the dependancies in the package.json.
"dependencies": {
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hook-form": "^7.63.0"
}
Important: This bundle has the /react-hook-form-action/
directory, which contains the PHP API endpoint code. Keep this directory in your PHP environment and change the target in the Axios request.
Then, execute npm run dev
command to start the development server to run this example.