How to Build a React Form - The Complete Guide (2025)
Everything you need to know about building React forms - from basic controlled inputs to validation, conditional logic, and publishing a live form that collects real responses.
In this guide
React forms are one of those things every developer builds dozens of times - contact forms, sign-up flows, feedback surveys, onboarding questionnaires. And yet, every time you start from scratch: the same useState, the same handleChange, the same boilerplate. This guide covers the right way to do it, from first principles to production.
1. The basics - controlled inputs with useState
React forms work with controlled inputs - the input value is driven by React state, not the DOM. This means you always know what is in the form at any point in time.
import { useState } from "react";
export default function ContactForm() {
const [name, setName] = useState("");
return (
<form>
<label>Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}The key is the value + onChange pair. Without value, the input is uncontrolled and React loses track of it.
2. Handling multiple fields without repetition
Once you have more than two fields, creating separate state for each gets messy. The standard pattern is a single object and a shared handleChange:
import { useState } from "react";
export default function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
return (
<form>
<input name="name" value={formData.name} onChange={handleChange} />
<input name="email" type="email" value={formData.email} onChange={handleChange} />
<textarea name="message" value={formData.message} onChange={handleChange} />
</form>
);
}The name attribute on each input maps to the key in your state object. This scales to any number of fields with no extra code.
3. Adding validation
For most forms, a combination of HTML5 native validation and a small errors object is enough. Avoid reaching for a validation library until you genuinely need it.
const [errors, setErrors] = useState({});
const validate = () => {
const errs = {};
if (!formData.name.trim()) errs.name = "Name is required";
if (!/S+@S+.S+/.test(formData.email)) errs.email = "Enter a valid email";
return errs;
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
// submit...
};Display errors inline beneath each field so the user knows exactly what to fix:
<input name="name" value={formData.name} onChange={handleChange} />
{errors.name && <p className="text-red-500 text-sm">{errors.name}</p>}4. All the field types you need
Each field type needs slightly different handling:
Checkbox
<input
type="checkbox"
name="agree"
checked={formData.agree}
onChange={handleChange}
/>Dropdown (select)
<select name="country" value={formData.country} onChange={handleChange}>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>Radio group
{["small", "medium", "large"].map((size) => (
<label key={size}>
<input
type="radio"
name="size"
value={size}
checked={formData.size === size}
onChange={handleChange}
/>
{size}
</label>
))}Date
<input
type="date"
name="dob"
value={formData.dob}
onChange={handleChange}
min="2000-01-01"
max="2025-12-31"
/>5. Conditional logic (show/hide fields)
Show a field only when another field has a specific value. This keeps long forms short by only asking for information that is relevant.
// Only show "Company name" if the user selects "Business"
{formData.accountType === "business" && (
<input
name="company"
placeholder="Company name"
value={formData.company}
onChange={handleChange}
/>
)}Remember to clear hidden field values when their condition becomes false - otherwise stale data gets submitted.
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => {
const next = { ...prev, [name]: value };
// Clear company when switching away from business
if (name === "accountType" && value !== "business") {
next.company = "";
}
return next;
});
};6. Handling form submission
The basic pattern - prevent default, validate, then send data to your API:
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) { setErrors(errs); return; }
setSubmitting(true);
try {
await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
setSubmitted(true);
} catch {
alert("Submission failed. Please try again.");
} finally {
setSubmitting(false);
}
};
if (submitted) return <p>Thank you! We will be in touch.</p>;7. Skipping the code - publish and collect responses
If you do not need the form embedded in your own app - a contact form, a survey, an intake form - you do not need to write any of the above. You can build the form visually and publish it with a shareable link that collects real responses into a dashboard.
ReactForm.co lets you do exactly that. Drag in fields, hit Publish, share the link. Responses show up in your dashboard in real time. Free plan includes 3 forms and 50 responses per form - no backend, no database, no credit card.
Build your form now - it is free
3 published forms, 50 responses each. No credit card.
8. Exporting a clean React component
If you do need the form embedded in your own codebase, you can use the builder to design the form visually, then hit Export to download a clean .jsx component - with useState, handleChange, and handleSubmit all wired up. Zero dependencies on this tool. Just copy it into your project.
// What you get when you export:
import { useState } from "react";
export default function ContactForm() {
const [formData, setFormData] = useState({
name: "", email: "", message: ""
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
{/* your fields here */}
<button type="submit">Submit</button>
</form>
);
}Summary
- Use controlled inputs -
value+onChangealways - One state object + one
handleChangescales to any form size - Validate before submit, display errors inline
- Handle checkbox with
e.target.checked, note.target.value - Conditional fields: render conditionally, clear values when hidden
- For forms that just need to collect responses - publish, do not embed
Skip the boilerplate - build your form visually
Drag fields, set validation, publish and collect responses - or export clean React code. Free to start, no credit card.
Open the Form Builder