Form Submission and Validation — Handling User Input

Form handling in React has two layers: collecting input values (covered in Chapter 35 with controlled inputs) and doing something with those values when the user submits. Submission involves preventing the browser’s default form reload, validating the data, calling the API, and handling both success and error responses. Client-side validation provides immediate feedback; server-side validation from FastAPI’s 422 responses catches anything the client missed. Wiring all three together — input, validation, and submission — is the core form pattern used throughout the blog application.

Form Submission Pattern

import { useState } from "react";

function ContactForm() {
    const [form, setForm]         = useState({ name: "", email: "", message: "" });
    const [errors, setErrors]     = useState({});
    const [isLoading, setLoading] = useState(false);
    const [success, setSuccess]   = useState(false);
    const [apiError, setApiError] = useState(null);

    // Generic change handler
    function handleChange(e) {
        const { name, value } = e.target;
        setForm((prev) => ({ ...prev, [name]: value }));
        // Clear field error as user types
        if (errors[name]) setErrors((prev) => ({ ...prev, [name]: null }));
    }

    // Client-side validation
    function validate() {
        const errs = {};
        if (!form.name.trim())           errs.name    = "Name is required";
        if (!form.email.includes("@"))   errs.email   = "Valid email required";
        if (form.message.length < 10)    errs.message = "Message must be at least 10 characters";
        return errs;
    }

    async function handleSubmit(e) {
        e.preventDefault();   // ← prevent browser reload!

        const errs = validate();
        if (Object.keys(errs).length) { setErrors(errs); return; }

        setLoading(true);
        setApiError(null);

        try {
            await submitContact(form);
            setSuccess(true);
            setForm({ name: "", email: "", message: "" });
        } catch (err) {
            setApiError(err.message);
        } finally {
            setLoading(false);
        }
    }

    return (
        <form onSubmit={handleSubmit} noValidate className="space-y-4">
            {apiError && (
                <div className="bg-red-50 text-red-700 px-4 py-2 rounded">{apiError}</div>
            )}
            {success && (
                <div className="bg-green-50 text-green-700 px-4 py-2 rounded">
                    Message sent!
                </div>
            )}
            <div>
                <input name="name" value={form.name} onChange={handleChange}
                       placeholder="Your name"
                       className={errors.name ? "border-red-500" : ""} />
                {errors.name && <p className="text-red-500 text-sm">{errors.name}</p>}
            </div>
            <button type="submit" disabled={isLoading}>
                {isLoading ? "Sending..." : "Send"}
            </button>
        </form>
    );
}
Note: The noValidate attribute on the <form> element disables the browser’s built-in HTML5 validation pop-ups (the little tooltip bubbles that appear on required inputs). Without noValidate, the browser and React both show validation — resulting in two different error styles. Since you are implementing custom React validation with your own error messages, noValidate prevents the browser from interfering.
Tip: Display server validation errors from FastAPI’s 422 response alongside the relevant form fields. The 422 body has a detail array where each item has a loc (field path like ["body", "email"]) and a msg. Map these to your form’s error state: const fieldErrors = {}; error.response.data.detail.forEach(e => { const field = e.loc[1]; fieldErrors[field] = e.msg; }); setErrors(fieldErrors);
Warning: Always use type="submit" on the submit button (not type="button") so pressing Enter in any input field submits the form — this is the expected browser behaviour and is important for keyboard accessibility. Using type="button" requires manually attaching an onClick handler to the button and misses the Enter key behaviour. The form’s onSubmit handler fires when either the submit button is clicked or Enter is pressed in any field.

Handling FastAPI Validation Errors

async function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    try {
        await api.post("/posts", form);
        navigate("/dashboard");
    } catch (err) {
        if (err.response?.status === 422) {
            // Map FastAPI field errors to form state
            const fieldErrors = {};
            err.response.data.detail.forEach((issue) => {
                // issue.loc = ["body", "title"] or ["body", "slug"]
                const field = issue.loc[issue.loc.length - 1];
                fieldErrors[field] = issue.msg;
            });
            setErrors(fieldErrors);
        } else if (err.response?.status === 409) {
            setErrors({ slug: "This slug is already taken" });
        } else {
            setApiError(err.response?.data?.detail ?? "Something went wrong");
        }
    } finally {
        setLoading(false);
    }
}

Common Mistakes

Mistake 1 — Forgetting e.preventDefault() (page reloads)

❌ Wrong — form causes full page reload:

async function handleSubmit() {   // missing e parameter!
    await api.post("/data", form);   // page reloads before this runs
}

✅ Correct:

async function handleSubmit(e) {
    e.preventDefault();   // ✓ stop browser reload
    await api.post("/data", form);
}

Mistake 2 — Not resetting loading state on error

❌ Wrong — button stays disabled forever on error:

setLoading(true);
try { await submit(); } catch (err) { setError(err.message); }
// missing setLoading(false) in catch or finally!

✅ Correct — always use finally:

try { await submit(); } catch (err) { setError(err.message); }
finally { setLoading(false); }   // ✓ always runs

Quick Reference

Task Code
Prevent reload e.preventDefault() in onSubmit
Disable HTML5 validation <form noValidate>
Submit button state <button disabled={isLoading}>
FastAPI 422 errors Map error.response.data.detail to field errors
Reset loading finally { setLoading(false); }
Field error display {errors.field && <p className="text-red-500">{errors.field}</p>}

🧠 Test Yourself

Your form’s submit handler calls an async API function and sets setLoading(true) before it. The API call throws an error. setLoading(false) is only called in the .then() callback. What happens to the submit button?