Form Handling
In Controlled Components, we bound a single input to state. This lesson covers handling a real form end to end -- submission, multiple fields, validation, and error messages.
Submitting a Form: Collecting Values with onSubmit
The onSubmit we saw in Events is usually used in forms to work with
the value that's already in state:
import { useState } from "react";
function FormSubmitExample() {
const [name, setName] = useState("");
const [submittedName, setSubmittedName] = useState(null);
function handleSubmit(event) {
event.preventDefault();
// Form gönderildiğinde, o an state'te olan değeri kullanıyoruz --
// controlled input olduğu için değerin ne olduğunu her zaman biliyoruz.
setSubmittedName(name);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="İsmini yaz"
/>
<button type="submit">Gönder</button>
{submittedName && <p>Gönderilen: {submittedName}</p>}
</form>
);
}
Since the input is controlled, the name state is already up to date
by the time the form is submitted -- there's no need to read the value
from the DOM, we just use state directly.
Managing Multiple Fields
When a form has more than one input, instead of opening a separate
useState for each one, it's more manageable to keep them all in ONE
state object:
import { useState } from "react";
function MultiFieldFormExample() {
// Her alan için ayrı bir useState açmak yerine, TÜM alanları tek bir
// state nesnesinde tutuyoruz -- form büyüdükçe daha yönetilebilir.
const [formData, setFormData] = useState({ name: "", email: "" });
function handleChange(event) {
const { name, value } = event.target;
// Hangi input değiştiyse (input'un `name` attribute'una bakarak),
// yalnızca o alanı güncelliyoruz, diğerlerini olduğu gibi kopyalıyoruz.
setFormData({ ...formData, [name]: value });
}
return (
<form>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="İsim"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="E-posta"
/>
<p>
{formData.name} / {formData.email}
</p>
</form>
);
}
event.target.name gives you the name attribute of whichever input
changed -- with [name]: value (a computed property name), a SINGLE
handleChange function can figure out which field changed and update
only that one. This follows the immutability rule from State:
{ ...formData, [name]: value } copies the old object and creates a
NEW object with just one field changed.
Simple Validation
Before submitting a form, you usually want to check whether the entered values are valid -- this is called validation:
import { useState } from "react";
function RequiredFieldValidationExample() {
const [name, setName] = useState("");
const [error, setError] = useState("");
function handleSubmit(event) {
event.preventDefault();
if (name.trim() === "") {
setError("İsim boş bırakılamaz.");
return;
}
setError("");
console.log("Form gönderildi:", name);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="İsim"
/>
<button type="submit">Gönder</button>
{error && <p className="error">{error}</p>}
</form>
);
}
We check the value at submit time (inside handleSubmit); if it's
empty, we write a message into the error state and exit the function
early (return) without actually "submitting" the form.
Displaying Error Messages
Once you find an error, you need to show it to the user -- the &&
pattern from Conditional Rendering fits perfectly here:
import { useState } from "react";
function EmailFormatValidationExample() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
// Basit bir e-posta biçimi kontrolü -- gerçek projelerde daha kapsamlı
// kurallar da olabilir, burada yalnızca "@" ve bir nokta var mı bakıyoruz.
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function handleSubmit(event) {
event.preventDefault();
if (!emailPattern.test(email)) {
setError("Geçerli bir e-posta adresi gir.");
return;
}
setError("");
console.log("Geçerli e-posta:", email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="E-posta"
/>
<button type="submit">Gönder</button>
{error && <p className="error">{error}</p>}
</form>
);
}
If error is non-empty, the expression {error && <p>...} renders the
error message; if error is empty, nothing renders. This example also
includes a simple email format check -- using a regex
(emailPattern.test(email)).
Validating the Whole Form Before Submit
In a form with multiple fields, each field may need to show its own error:
import { useState } from "react";
function FullFormValidationExample() {
const [formData, setFormData] = useState({ name: "", email: "" });
const [errors, setErrors] = useState({});
function handleChange(event) {
const { name, value } = event.target;
setFormData({ ...formData, [name]: value });
}
function validate() {
// Her alan için ayrı bir hata mesajı üretip, hataların HEPSİNİ tek bir
// nesnede topluyoruz -- her alanın kendi hatasını göstermesini sağlar.
const newErrors = {};
if (formData.name.trim() === "") {
newErrors.name = "İsim boş bırakılamaz.";
}
if (formData.email.trim() === "") {
newErrors.email = "E-posta boş bırakılamaz.";
}
return newErrors;
}
function handleSubmit(event) {
event.preventDefault();
const newErrors = validate();
setErrors(newErrors);
// Nesnenin hiç anahtarı yoksa, hiç hata yok demektir.
if (Object.keys(newErrors).length === 0) {
console.log("Form geçerli, gönderiliyor:", formData);
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="İsim"
/>
{errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="E-posta"
/>
{errors.email && <p className="error">{errors.email}</p>}
</div>
<button type="submit">Gönder</button>
</form>
);
}
The validate() function produces a separate error message for each
field and collects them all into ONE object (newErrors). If that
object has no keys (Object.keys(newErrors).length === 0), the form is
valid. Under each input, only its OWN error (errors.name,
errors.email) is shown -- again with && for conditional rendering.
Summary and Glossary
A form is submitted with onSubmit; because the inputs are controlled,
their values are already ready in state by the time of submission.
Multiple fields are kept in a single state object, with
event.target.name used to figure out which field changed. Validation
is usually done at submit time; errors are kept in state (as a single
message, or an object with one entry per field) and shown with && for
conditional rendering.
Glossary
Validation — The process of checking whether entered values follow the expected rules.
Computed Property Name — Syntax like { [name]: value } that lets
you set an object's key dynamically from a variable.
Practical Project
There's a real, runnable example project that brings together the concepts from this category (Controlled Components, Form Handling): Forms Demo -- a simple sign-up form.
It shows controlled inputs with value+onChange, managing multiple
fields in a single state object, onSubmit+preventDefault, and
validating the whole form before submit with each field showing its own
error, all working together. You can download it and run it yourself,
and read through the code line by line:
git clone https://github.com/cdurgun/react-course-projects.git
cd react-course-projects
npm install
cd projects/forms
npm run dev
The react-course-projects repo uses npm workspaces -- npm install
only needs to run once, at the repo root, and every project folder
shares the same dependencies (no separate node_modules per folder). If
you've already run npm install at the root, you can just
cd react-course-projects/projects/forms and run npm run dev.