Form Handling

Submitting a form with onSubmit, managing multiple fields with a single state object, validation, and displaying error messages -- with simple examples.

Intermediate 13 min
TR

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.