Form validation is a core part of building reliable user interfaces. In React, it can be implemented in a clean and flexible way using simple state management and regular expressions (regex). Regex allows you to express validation rules as patterns, for example to check whether an email looks valid, a name has enough characters, or a phone number only contains allowed symbols.
A common pattern in React is to keep the form values and validation errors in state, update them on each change, and run a final validation pass when the user submits the form. Below is a basic but solid example of a contact form with four fields: full name, email, phone, and message. Each field uses one or more regex-based rules to decide whether the input is valid.
import React, { useState } from "react";
const ContactFormWithValidation = () => {
const [formData, setFormData] = useState({
fullName: "",
email: "",
phone: "",
message: "",
});
const [errors, setErrors] = useState({});
// Basic regex patterns
const nameRegex = /^[A-Za-zÀ-ÖØ-öø-ÿ\s]{3,}$/; // at least 3 letters/spaces
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // simple email pattern
const phoneRegex = /^[0-9+\s-]{7,}$/; // digits, spaces, + and -, min 7 chars
const validateField = (name, value) => {
let error = "";
if (name === "fullName") {
if (!value.trim()) {
error = "Full name is required.";
} else if (!nameRegex.test(value)) {
error = "Please enter at least 3 letters (letters and spaces only).";
}
}
if (name === "email") {
if (!value.trim()) {
error = "Email is required.";
} else if (!emailRegex.test(value)) {
error = "Please enter a valid email address.";
}
}
if (name === "phone") {
// phone is optional, validate only when not empty
if (value && !phoneRegex.test(value)) {
error = "Phone can contain digits, spaces, + and - (min 7 characters).";
}
}
if (name === "message") {
if (!value.trim()) {
error = "Message is required.";
} else if (value.trim().length < 10) {
error = "Message should be at least 10 characters long.";
}
}
return error;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// Live validation on each change
const errorMsg = validateField(name, value);
setErrors((prev) => ({
...prev,
[name]: errorMsg,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
// Validate all fields on submit
const newErrors = {};
Object.entries(formData).forEach(([key, value]) => {
const err = validateField(key, value);
if (err) {
newErrors[key] = err;
}
});
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) {
console.log("Form has errors ❌", newErrors);
return;
}
console.log("Form is valid ✅", formData);
// Here you could send formData to an API, json-server, Firebase, etc.
// After successful submit you may want to clear the form:
setFormData({
fullName: "",
email: "",
phone: "",
message: "",
});
};
return (
<form className="kontakt-form" onSubmit={handleSubmit}>
{/* Full Name */}
<div className="kontakt-input-group">
<label htmlFor="fullName">Full Name</label>
<input
type="text"
id="fullName"
name="fullName"
value={formData.fullName}
onChange={handleChange}
placeholder="Your full name"
/>
{errors.fullName && (
<p className="form-error">{errors.fullName}</p>
)}
</div>
{/* Email */}
<div className="kontakt-input-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="example@mail.com"
/>
{errors.email && (
<p className="form-error">{errors.email}</p>
)}
</div>
{/* Phone (optional) */}
<div className="kontakt-input-group">
<label htmlFor="phone">Phone (optional)</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="+49 123 456 789"
/>
{errors.phone && (
<p className="form-error">{errors.phone}</p>
)}
</div>
{/* Message */}
<div className="kontakt-input-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
rows="4"
value={formData.message}
onChange={handleChange}
placeholder="How can I help you?"
/>
{errors.message && (
<p className="form-error">{errors.message}</p>
)}
</div>
<button type="submit" className="kontakt-submit">
Send Message
</button>
</form>
);
};
export default ContactFormWithValidation;
In this example, each regex pattern is responsible for a specific type of validation. The name regex enforces at least three characters and restricts input to letters and spaces, which works well for a simple full name check. The email regex confirms that the value contains an @ symbol and a domain-like part, which is often sufficient for basic email validation. The phone regex accepts digits, spaces, the plus sign, and dashes, with a minimum length to avoid obviously invalid input. These rules can be adjusted to match the requirements of your application.
To make validation messages more user-friendly and consistent with the design of your app, a small amount of CSS can be added. This improves readability and helps users clearly see what needs to be fixed.
.form-error {
margin-top: 0.3rem;
font-size: 0.8rem;
color: #ef4444; /* red tone for errors */
}
.kontakt-input-group {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
}
.kontakt-input-group label {
margin-bottom: 0.25rem;
font-weight: 500;
}
.kontakt-input-group input,
.kontakt-input-group textarea {
padding: 0.6rem 0.8rem;
border-radius: 8px;
border: 1px solid #d1d5db; /* light gray */
font-size: 0.95rem;
outline: none;
}
.kontakt-input-group input:focus,
.kontakt-input-group textarea:focus {
border-color: #6366f1; /* indigo tone */
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.25);
}
.kontakt-submit {
margin-top: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #ffffff;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.kontakt-submit:hover {
transform: translateY(-1px);
box-shadow: 0 8px 18px rgba(79, 70, 229, 0.35);
}
This combination of React state, basic regex patterns, and a little styling provides a solid foundation for building validated forms. It avoids heavy external libraries while keeping the logic explicit and easy to adjust. As your application grows, you can extend these patterns, extract the validation logic into reusable hooks, or gradually adopt more advanced solutions depending on your needs.

