React Form with Validation
Validation does not require a library for most forms. A plain validate function that returns an errors object handles required fields, email format, length limits, and custom rules with zero dependencies. Blur-triggered errors give users feedback at the right moment — after they leave a field — rather than bombarding them with errors before they start.
The challenge
Form validation in React usually requires extra libraries or complex logic that is hard to maintain as requirements change.
- Showing errors only after a user leaves a field (not before they start typing) requires tracking "touched" state
- Email regex needs to be reliable without being so strict it rejects valid addresses
- Clearing an error message the moment the user corrects the field requires re-running validation on every change
- Deciding when to validate — on submit, on blur, or on change — affects perceived UX significantly
Working code example
Here is a complete, self-contained working example you can drop directly into any React project. It uses Tailwind CSS for styling and requires no external dependencies beyond React itself.
import { useState } from 'react';
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validate(data) {
const errs = {};
if (!data.name.trim()) errs.name = 'Name is required';
if (!data.email.trim()) errs.email = 'Email is required';
else if (!EMAIL_RE.test(data.email)) errs.email = 'Enter a valid email address';
if (!data.message.trim()) errs.message = 'Message is required';
else if (data.message.trim().length < 10) errs.message = 'Message must be at least 10 characters';
return errs;
}
export default function ValidatedForm() {
const [form, setForm] = useState({ name: '', email: '', message: '' });
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [submitted, setSubmitted] = useState(false);
const handleChange = (e) => {
const next = { ...form, [e.target.name]: e.target.value };
setForm(next);
if (touched[e.target.name]) setErrors(validate(next));
};
const handleBlur = (e) => {
setTouched((t) => ({ ...t, [e.target.name]: true }));
setErrors(validate(form));
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate(form);
setErrors(errs);
setTouched({ name: true, email: true, message: true });
if (!Object.keys(errs).length) setSubmitted(true);
};
if (submitted)
return <p className="p-6 text-green-600 font-medium">Message sent!</p>;
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-4 bg-white rounded-2xl shadow">
<h2 className="text-xl font-bold text-gray-800">Send us a message</h2>
{['name', 'email'].map((field) => (
<div key={field}>
<label className="block text-sm font-medium text-gray-700 mb-1 capitalize">{field}</label>
<input
name={field}
value={form[field]}
onChange={handleChange}
onBlur={handleBlur}
className={`w-full border rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500 ${errors[field] && touched[field] ? 'border-red-400' : 'border-gray-300'}`}
/>
{errors[field] && touched[field] && (
<p className="text-red-500 text-xs mt-1">{errors[field]}</p>
)}
</div>
))}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Message</label>
<textarea
name="message"
value={form.message}
onChange={handleChange}
onBlur={handleBlur}
rows={4}
className={`w-full border rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500 ${errors.message && touched.message ? 'border-red-400' : 'border-gray-300'}`}
/>
{errors.message && touched.message && (
<p className="text-red-500 text-xs mt-1">{errors.message}</p>
)}
</div>
<button type="submit" className="w-full bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition">
Send Message
</button>
</form>
);
}How ReactForm.co helps
ReactForm's visual builder handles all of the above — validation configuration, validation rules, state management, and responsive layout — without writing a single line of code. Drag fields onto the canvas, configure their properties in the sidebar, and get production-ready React output. You can publish the form and collect responses instantly, or export the JSX to drop into your own codebase.
Build this form visuallyFrequently asked questions
Should I validate on change, on blur, or on submit?
The best UX is a combination: validate on blur so users get feedback after leaving a field, then re-validate on change once the field has been touched so errors clear the moment they are fixed. Always validate on submit as a final gate. Validating on every keystroke before a user has finished typing a field shows errors too early and feels aggressive.
Do I need Yup or Zod for form validation?
Not for most forms. Yup and Zod are excellent for complex schemas, async validation, and sharing validation rules between frontend and backend. But for a contact form or sign-up form with 5–8 fields, a plain validate function is smaller, faster, and easier to read. Add a library when your validation logic genuinely outgrows a plain function.
How do I show a generic error summary at the top of the form?
After submit, check if Object.keys(errors).length > 0 and render a banner with the count or list of errors. Map over Object.entries(errors) to list them. This helps screen reader users who may not notice inline errors below individual fields.
Related topics
Build this form visually — no code needed
ReactForm.co handles validation fields, validation, conditional logic, and responsive layout automatically. Publish in minutes and collect responses for free.

