Contact forms are common in React applications, but a good form needs more than a few input fields. It should manage user input, validate required fields, display helpful error messages, and prevent invalid submissions.
In this tutorial, we'll build a reusable contact form using React functional components and the useState hook.
What we'll build
Our form will include:
- Name, email, and message fields
- Required-field validation
- Basic email validation
- Clear error messages
- A success message after valid submission
- A reusable component that can be adapted for different projects
We'll keep the example focused on React without adding unnecessary libraries.
Step 1: Create a React project
If you don't already have a React project, you can create one with Vite:
npm create vite@latest react-contact-form -- --template react
cd react-contact-form
npm install
npm run dev
Open the local URL displayed in your terminal.
Step 2: Create the contact form component
Create a file named ContactForm.jsx inside your src folder.
We'll use React's useState hook to store form values and validation errors.
import { useState } from "react";
const initialForm = {
name: "",
email: "",
message: "",
};
function ContactForm() {
const [form, setForm] = useState(initialForm);
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
function handleChange(event) {
const { name, value } = event.target;
setForm((previousForm) => ({
...previousForm,
[name]: value,
}));
setErrors((previousErrors) => ({
...previousErrors,
[name]: "",
}));
setSubmitted(false);
}
function validateForm() {
const newErrors = {};
if (!form.name.trim()) {
newErrors.name = "Name is required.";
}
if (!form.email.trim()) {
newErrors.email = "Email is required.";
} else if (
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)
) {
newErrors.email = "Enter a valid email address.";
}
if (!form.message.trim()) {
newErrors.message = "Message is required.";
}
return newErrors;
}
function handleSubmit(event) {
event.preventDefault();
const validationErrors = validateForm();
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
setSubmitted(false);
return;
}
setSubmitted(true);
setForm(initialForm);
}
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
value={form.name}
onChange={handleChange}
aria-invalid={Boolean(errors.name)}
aria-describedby={errors.name ? "name-error" : undefined}
/>
{errors.name && (
<p id="name-error" role="alert">
{errors.name}
</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={handleChange}
aria-invalid={Boolean(errors.email)}
aria-describedby={
errors.email ? "email-error" : undefined
}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email}
</p>
)}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={form.message}
onChange={handleChange}
aria-invalid={Boolean(errors.message)}
aria-describedby={
errors.message ? "message-error" : undefined
}
/>
{errors.message && (
<p id="message-error" role="alert">
{errors.message}
</p>
)}
</div>
<button type="submit">Send Message</button>
{submitted && (
<p role="status">
Form validation successful!
</p>
)}
</form>
);
}
export default ContactForm;
Step 3: Render the component
Open src/App.jsx and import the component:
import ContactForm from "./ContactForm";
function App() {
return (
<main>
<h1>Contact Us</h1>
<p>Fill out the form below to get started.</p>
<ContactForm />
</main>
);
}
export default App;
You should now see the contact form in your browser.
Try submitting it without filling in any fields. The form should display validation errors. Enter valid-looking details and submit again to see the success message.
Step 4: Understand how the validation works
Controlled inputs
Each input gets its value from React state:
value={form.email}
onChange={handleChange}
This keeps the displayed value and the component's state in sync.
Updating one field
The spread operator preserves the other fields when a single input changes:
setForm((previousForm) => ({
...previousForm,
[name]: value,
}));
Without preserving the previous values, updating one field could accidentally replace the entire form object.
Validating before submission
The validateForm() function checks each field and returns an object containing any errors.
The submit handler stops when errors exist:
if (Object.keys(validationErrors).length > 0) {
setSubmitted(false);
return;
}
This separates validation logic from the JSX and makes the component easier to maintain.
Step 5: Important improvements for a real application
This example demonstrates client-side validation, but it is not a complete production contact system.
Before using it on a live website, consider the following improvements:
- Connect a backend:Send valid submissions to an API endpoint instead of simply showing a success message.
- Validate on the server: Never rely on browser-side validation alone. Validate and sanitize submitted data on the server.
- Handle loading and errors: Disable repeated submissions while the request is processing and display useful messages if it fails.
- Protect the endpoint: Add appropriate rate limiting and spam protection.
- Store submissions securely:If you save enquiries in a database, define an appropriate retention and access policy.
- Improve accessibility: Ensure error messages, keyboard navigation, and focus handling work well for all users.
Also note that the email regular expression above is a simple check, not a complete email-address validation standard.
Conclusion
Building a React contact form is a useful exercise for understanding state management, controlled inputs, event handling, conditional rendering, and validation.
The same structure can be extended for registration forms, booking requests, feedback forms, and other user-input workflows.
Try this next: Add a loading state and connect the form to a backend API. Then handle successful responses, server validation errors, and network failures separately.
Disclosure: This article was prepared with AI assistance and should be reviewed and tested by the author before publication.
Top comments (0)