Field-Level Validation in React Forms
📌 Problem Statement
In modern web applications, form validation is critical for user experience and data integrity. Many applications suffer from:
We need a scalable and reusable solution for field-level validation with visual and accessible feedback.
🎯 Objective
✅ Requirements
🗺️ Flow Diagram
📁 File Structure
/hooks
├── useFormField.js → Handles single field state + validation
└── useFormManager.js → Manages all fields & submit behavior
/components
├── FormField.jsx → Generic field renderer
└── ErrorMessage.jsx → Accessible error UI
🔧 Step-by-Step Implementation
1️⃣ useFormField.js
import { useState, useCallback } from "react";
export function useFormField(initialValue = "", validators = []) {
const [value, setValue] = useState(initialValue);
const [touched, setTouched] = useState(false);
const [error, setError] = useState(null);
const validate = useCallback(() => {
for (const rule of validators) {
const result = rule(value);
if (result) {
setError(result);
return false;
}
}
setError(null);
return true;
}, [value, validators]);
const onChange = (e) => {
setValue(e.target.value);
if (touched) validate();
};
const onBlur = () => {
setTouched(true);
validate();
};
return {
value,
error,
touched,
onChange,
onBlur,
validate,
setValue,
};
}
2️⃣ useFormManager.js
export function useFormManager(fields) {
const validateForm = () => {
let isValid = true;
for (const field of Object.values(fields)) {
const valid = field.validate();
if (!valid && isValid) {
// focus the first invalid field
document.getElementById(field.id)?.focus();
}
isValid = isValid && valid;
}
return isValid;
};
return {
validateForm,
};
}
3️⃣ ErrorMessage.jsx
const ErrorMessage = ({ error, id }) => {
if (!error) return null;
return (
<p id={id} className="mt-1 text-sm text-red-600" role="alert">
{error}
</p>
);
};
export default ErrorMessage;
4️⃣ FormField.jsx
import ErrorMessage from "./ErrorMessage";
const FormField = ({
label,
name,
field,
type = "text",
as = "input",
options = [],
}) => {
const { value, onChange, onBlur, error } = field;
const sharedProps = {
id: name,
name,
value,
onChange,
onBlur,
"aria-invalid": !!error,
"aria-describedby": `${name}-error`,
className: `mt-1 block w-full p-2 border rounded-md shadow-sm focus:outline-none focus:ring ${
error ? "border-red-500 focus:ring-red-300" : "border-gray-300 focus:ring-blue-300"
}`,
};
let element;
if (as === "select") {
element = (
<select {...sharedProps}>
<option value="">-- Select --</option>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
} else if (as === "textarea") {
element = <textarea rows={4} {...sharedProps} />;
} else {
element = <input type={type} {...sharedProps} />;
}
return (
<div className="mb-5">
{label && (
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
{element}
<ErrorMessage error={error} id={`${name}-error`} />
</div>
);
};
export default FormField;
5️⃣ RegistrationForm.jsx
import React from "react";
import FormField from "./components/FormField";
import { required, minLength, isEmail } from "./validators";
import { useFormField } from "./hooks/useFormField";
import { useFormManager } from "./hooks/useFormManager";
export default function RegistrationForm() {
const username = useFormField("", [required(), minLength(3)]);
const email = useFormField("", [required(), isEmail()]);
const role = useFormField("", [required("Please choose a role")]);
const form = { username, email, role };
const { validateForm } = useFormManager(form);
const handleSubmit = (e) => {
e.preventDefault();
if (!validateForm()) return;
console.log("Form submitted", {
username: username.value,
email: email.value,
role: role.value,
});
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto mt-8">
<FormField label="Username" name="username" field={username} />
<FormField label="Email" name="email" type="email" field={email} />
<FormField
label="Role"
name="role"
field={role}
as="select"
options={[
{ label: "Admin", value: "admin" },
{ label: "User", value: "user" },
]}
/>
<button
type="submit"
className="mt-4 w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
>
Submit
</button>
</form>
);
}
🧠 Design Highlights
🚀 Further Extensions
✅ Conclusion
This approach gives you a clean and centralized solution for handling all form fields with consistent error handling and visual feedback. It’s scalable, readable, and perfect for large codebases or design systems.