Form & Input Bug Scenarios
Ten real-world bug tickets about form behavior, input controls, validation, and submission. Each entry shows the non-technical ticket, the diagnosis, a concise fix, and why the fix is correct.
How to Use This List
- Form bugs cluster around four causes: missing onChange (controlled), wrong handler signature, wrong input type, and missing default values.
- Always check whether the input is controlled, uncontrolled, or a hybrid - most form bugs hide in that boundary.
1. "I can't type in the email field on the signup form"
"Tried to sign up. The email box won't accept any input - I press keys and nothing happens. Other fields work."
Diagnosis: The input has a value prop but no onChange, making it a read-only controlled input.
Fix:
"use client";
import { useState } from "react";
export function EmailField() {
const [email, setEmail] = useState("");
return (
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
}Why this works: React requires both value and onChange for controlled inputs. The onChange is what actually mutates state on each keystroke, which then re-renders the input with the new value.
2. "Clicking the Submit button reloads the page and loses all my answers"
"Filling out the survey, hit Submit at the bottom, the whole page reloads and my answers are gone."
Diagnosis: A <form> without onSubmit falls back to native form submission, which navigates the browser to the form's action (or current URL).
Fix:
"use client";
import { FormEvent, useState } from "react";
export function Survey() {
const [name, setName] = useState("");
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
saveSurvey({ name });
};
return (
<form onSubmit={onSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}Why this works: e.preventDefault() cancels the native submit, so the browser does not reload. The handler runs your save logic with the current form state instead of throwing the data away.
3. "When I type fast in the search field my cursor jumps to the end of the text"
"I edit a name in the middle, type one character, and the cursor jumps to the end. Driving the team crazy."
Diagnosis: The input is being unmounted and remounted on every keystroke - usually because it's defined inside a parent component that re-renders, or because of a changing key.
Fix:
"use client";
// Define stable components OUTSIDE re-rendering parents
function NameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
export function Page() {
const [name, setName] = useState("");
return <NameInput value={name} onChange={setName} />;
}Why this works: Components defined inside another component are recreated on every render, so React treats them as new types and unmounts/remounts the input - losing selection state. Defining the component at module scope keeps the input instance stable across renders.
4. "The form clears when I submit an invalid email - all my other fields are wiped"
"Big checkout form. If the email is invalid we show the error, but every other field empties out at the same time."
Diagnosis: The form uses defaultValue on uncontrolled inputs without useForm defaults, then re-renders with new keys or remounts on validation, clearing the DOM state.
Fix:
"use client";
import { useForm } from "react-hook-form";
interface CheckoutForm {
email: string;
name: string;
address: string;
}
export function Checkout() {
const { register, handleSubmit, formState: { errors } } = useForm<CheckoutForm>({
defaultValues: { email: "", name: "", address: "" },
});
return (
<form onSubmit={handleSubmit((data) => save(data))}>
<input {...register("email", { pattern: /^[^@]+@[^@]+$/ })} />
{errors.email && <p>Invalid email</p>}
<input {...register("name")} />
<input {...register("address")} />
<button type="submit">Pay</button>
</form>
);
}Why this works: React Hook Form owns the field state in a ref-backed store, so validation errors trigger re-renders without remounting inputs or losing their values. Field values survive every validation cycle.
5. "The 'Remember me' checkbox always submits as checked even when I uncheck it"
"Login form's 'Remember me' is stuck. The state shows it as unchecked visually but the API always receives true."
Diagnosis: The checkbox is bound with value instead of checked, so React renders the value attribute as a string but never actually controls the checked state.
Fix:
"use client";
import { useState } from "react";
export function RememberMe() {
const [remember, setRemember] = useState(false);
return (
<label>
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
/>
Remember me
</label>
);
}Why this works: Checkboxes use checked and e.target.checked - not value and e.target.value. The value attribute on a checkbox is the literal string sent on submit when checked, not whether it's selected.
6. "The 'age' field saves as text instead of a number, and our age filter breaks"
"Customer support says the age field accepts text. Reports show ages like '42' in quotes - filters expecting numbers fail."
Diagnosis: <input type="number"> still returns a string from e.target.value. The value must be converted explicitly.
Fix:
"use client";
import { useState } from "react";
export function AgeField() {
const [age, setAge] = useState<number | null>(null);
return (
<input
type="number"
value={age ?? ""}
onChange={(e) => {
const v = e.target.valueAsNumber;
setAge(Number.isNaN(v) ? null : v);
}}
/>
);
}Why this works: valueAsNumber returns a number or NaN directly. Storing null when the field is empty avoids coercing empty strings to 0 and matches how a JSON API expects a missing value.
7. "Birthdays show up off by one day in the customer record"
"We pick a birthday in the date picker, save it, and the customer detail page shows the day before. Reproduces for everyone."
Diagnosis: new Date("1990-05-14") parses as UTC midnight, but toLocaleDateString() renders in local time - which is the previous day in any timezone west of UTC.
Fix:
export function formatBirthday(iso: string): string {
const [y, m, d] = iso.split("-").map(Number);
return new Date(y, m - 1, d).toLocaleDateString();
}Why this works: Constructing the Date with numeric args creates it in local time, matching the date the user actually picked. ISO strings imply UTC and silently shift across timezone boundaries - the calendar-date case needs a calendar-date parser.
8. "Hitting Enter on the search bar fires the search twice"
"Search bar at the top - if I press Enter the network tab shows two requests with the same query."
Diagnosis: Both onKeyDown (Enter) and the implicit form submit fire, each triggering its own search call.
Fix:
"use client";
import { FormEvent, useState } from "react";
export function SearchBar() {
const [q, setQ] = useState("");
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
search(q);
};
return (
<form onSubmit={onSubmit}>
<input value={q} onChange={(e) => setQ(e.target.value)} />
<button type="submit">Go</button>
</form>
);
}Why this works: Pressing Enter inside a form already submits it - there's no need to listen for Enter separately. Removing the onKeyDown handler and relying on onSubmit makes the single submit path the only path.
9. "Disabled fields aren't included in the data we save"
"We disable certain fields once they're 'locked.' On save, those fields are missing from the request body even though they're visible."
Diagnosis: Native FormData skips disabled inputs by design - they are not considered part of the form's submission.
Fix:
// Either:
<input name="planId" value={planId} readOnly />
// Or include the value explicitly when building the payload:
const payload = { ...Object.fromEntries(new FormData(form)), planId };Why this works: readOnly keeps the field non-editable but still submittable, while disabled removes it from FormData entirely. If the UX needs disabled, the submit handler must merge the missing values back in manually.
10. "The file upload bar never moves - it jumps from 0% to 100%"
"Customers can attach files but the progress bar is broken. It sits at 0%, then jumps to 100% when the upload finishes."
Diagnosis: The upload uses fetch, which doesn't expose request-side progress events. Only XMLHttpRequest (or a streaming-aware abstraction) reports upload bytes.
Fix:
"use client";
export function uploadWithProgress(file: File, onProgress: (pct: number) => void): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
});
xhr.addEventListener("load", () => (xhr.status < 400 ? resolve() : reject(new Error(xhr.statusText))));
xhr.addEventListener("error", () => reject(new Error("Network error")));
xhr.open("POST", "/api/upload");
const fd = new FormData();
fd.append("file", file);
xhr.send(fd);
});
}Why this works: xhr.upload.progress is the only browser API that reports request-side bytes-sent. Fetch's ReadableStream request bodies don't surface progress in any cross-browser way today, so XHR is still the right tool for upload progress UIs.
Related
- Forms & Validation Basics — Foundational form patterns.
- React Hook Form Recipes — Library-specific patterns.
- React Events — Event handler typing and behavior.