Quick Answer

If a React input has a value prop but no onChange handler, React treats it as read-only and refuses to let you type. Give it onChange to make it controlled, use defaultValue instead of value to make it uncontrolled, or add readOnly if that was the intent. Controlled inputs keep the field's text in state so React always owns the value; uncontrolled inputs let the DOM keep it and you read it on submit.

The dead input: value without onChange

Every React beginner hits this within their first week:

function Signup() {
  const [name, setName] = useState("");
  return <input value={name} />;   // typing does nothing
}

The field is frozen. The keyboard works everywhere else on the page. Nothing is broken, and React has already told you what is wrong in the console: you provided a value prop to a form field without an onChange handler.

Here is the mechanism. A normal HTML input owns its own value; you type, and the browser updates it. The moment you pass value to a React input, you are telling React that your variable is the truth. Every render React writes that variable back into the DOM node. You type "P", the browser shows "P" for an instant, React re-renders with name still an empty string, and the field is wiped. The only way the letter survives is if your handler puts it into state first.

<input value={name} onChange={(e) => setName(e.target.value)} />

There are three legitimate fixes and you should pick deliberately. Add onChange if you want a controlled field. Use defaultValue instead of value if you only wanted to prefill it. Add readOnly if the field really should not be editable, for example an order ID.

A second warning comes from the same area: "a component is changing an uncontrolled input to be controlled". That happens when the initial state is undefined (often from an API response that has not arrived) and later becomes a string. Initialise text fields to "", checkboxes to false, and never to null or undefined.

Controlled vs uncontrolled, and when each wins

A controlled input stores its text in React state. Every keystroke calls setState and re-renders the component. An uncontrolled input leaves the text in the DOM, and you read it only when you need it, usually on submit.

// controlled
const [city, setCity] = useState("");
<input value={city} onChange={(e) => setCity(e.target.value)} />

// uncontrolled
const cityRef = useRef(null);
<input ref={cityRef} defaultValue="Pune" />
// later: cityRef.current.value

Controlled is the default advice for a reason. Because the value lives in state, you can react to it as it changes: disable the submit button until the form is valid, show a character counter, format a phone number, filter a dropdown, or mirror one field into another. You can also reset the whole form by setting state back to the initial object.

Uncontrolled is not a mistake, though. It is the lighter option when you truly only care about the value once. A login form with two fields and no live validation does not need a re-render on every character. It is also how you handle <input type="file">, which is always uncontrolled because JavaScript cannot set its value for security reasons:

<input type="file" ref={fileRef} accept="image/*" />
// on submit: fileRef.current.files[0]

One habit worth forming early: do not mix the two on the same field. Passing both value and defaultValue, or switching an input from one mode to the other mid-life, produces warnings and unpredictable behaviour. Decide per field and stay there.

It is also worth knowing that React's onChange is not the DOM change event. Native change fires when the field loses focus; React wires up the input event instead, so onChange fires on every keystroke. If you actually want the on-blur behaviour, use onBlur.

One handler for many fields

Writing a separate useState and a separate handler for each of eight fields gets tedious fast. Keep one object in state and give every input a name that matches its key:

const [form, setForm] = useState({
  name: "", email: "", phone: "", city: "Pune", agree: false
});

function handleChange(e) {
  const { name, value, type, checked } = e.target;
  setForm((prev) => ({ ...prev, [name]: type === "checkbox" ? checked : value }));
}

<input name="name" value={form.name} onChange={handleChange} />
<input name="email" type="email" value={form.email} onChange={handleChange} />
<input name="phone" value={form.phone} onChange={handleChange} />
<select name="city" value={form.city} onChange={handleChange}>
  <option>Pune</option>
  <option>Chennai</option>
</select>
<input name="agree" type="checkbox" checked={form.agree} onChange={handleChange} />

Three details make or break this pattern. First, the computed key [name] needs square brackets; without them you create a literal property called name and every field overwrites the same slot. Second, checkboxes use checked, not value, and read e.target.checked. Third, always spread prev and use the updater form, so two fast updates in the same tick do not discard each other.

Two more traps. A <select> in React takes value on the select itself, not selected on an option. And e.target.value is always a string, even for type="number", so form.age + 1 gives "181" rather than 19. Convert at the point of use, and remember that an empty number field gives "", which Number("") turns into 0 rather than an error.

For radio groups, all the inputs share one name and each gets checked={form.plan === "annual"} with its own value. The same handler still works.

Validation that runs at the right time

Validation logic is easy; validation timing is what makes forms feel good or awful. Showing "invalid email" after the user has typed one character is aggressive and teaches people to ignore your messages. The pattern that works: validate on submit, and after a field has been touched once, also validate it on blur and while typing.

const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});

function validate(values) {
  const e = {};
  if (!values.name.trim()) e.name = "Name is required";
  if (!/^\S+@\S+\.\S+$/.test(values.email)) e.email = "Enter a valid email";
  if (!/^[6-9]\d{9}$/.test(values.phone)) e.phone = "Enter a 10-digit mobile number";
  if (!values.agree) e.agree = "Please accept the terms";
  return e;
}

function handleBlur(e) {
  setTouched((t) => ({ ...t, [e.target.name]: true }));
  setErrors(validate(form));
}

Then render {touched.phone && errors.phone && <p>{errors.phone}</p>}. Indian mobile numbers start with 6 to 9 and have ten digits, which is what that pattern encodes; strip spaces and a leading +91 before testing, because users paste both.

Do not rely on the regex alone for email. No practical pattern matches the real specification, and a typo like gmial.com passes any of them. The only real check is sending a verification mail. Keep the client-side rule loose and catch the rest on the server, where validation must be repeated anyway: anyone can open devtools and submit whatever they like.

The browser's own validation is free and worth using for the obvious cases. required, type="email", minLength and pattern give you keyboard hints on mobile and block submission with no JavaScript. If you want your own messages instead, put noValidate on the <form> and keep the attributes for the mobile keyboard behaviour.

Submitting without losing the user's data

The classic React form bug after the dead input is the page that reloads and wipes everything. A <button> inside a <form> defaults to type="submit", and a submitted form navigates unless you stop it:

// handle submit on the form element, not with onClick on the button
<form onSubmit={handleSubmit}>
  {/* the fields go here */}
  <button type="submit" disabled={submitting}>
    {submitting ? "Sending..." : "Register"}
  </button>
  <button type="button" onClick={reset}>Clear</button>
</form>

Note the second button. Any button inside a form that is not meant to submit must say type="button", otherwise clicking "Clear" submits the form. Handling submit on the form rather than the button also means the Enter key works, which is what most users expect.

async function handleSubmit(e) {
  e.preventDefault();
  const found = validate(form);
  setErrors(found);
  setTouched({ name: true, email: true, phone: true, agree: true });
  if (Object.keys(found).length > 0) return;

  setSubmitting(true);
  try {
    const res = await fetch("/api/register", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(form)
    });
    if (!res.ok) throw new Error("Registration failed");
    setDone(true);
  } catch (err) {
    setServerError(err.message);   // keep form state, do not clear
  } finally {
    setSubmitting(false);
  }
}

Two things save you real support requests. Disable the button while the request is in flight, or a slow connection on a college network produces three duplicate registrations. And never clear the fields when the server returns an error: the user typed all of it once, and asking them to do it again because your API was down is how forms get abandoned.

Finally, know when to stop hand-rolling. Once a form has twenty fields, conditional sections, and rules that depend on other answers, a form library will do this better than the code you write on a deadline.

Frequently Asked Questions

Why can I not type in my React input? Because it has a value prop and no onChange handler, which makes React treat it as a read-only controlled field. React rewrites the DOM value from your state on every render, so any character the browser inserts is immediately overwritten. Add onChange to update state, switch to defaultValue if you only wanted to prefill it, or add readOnly if the field genuinely should not be edited.
Should I use controlled or uncontrolled inputs? Use controlled inputs when the value affects the rest of the UI: live validation, a disabled submit button, a character counter, dependent fields or search-as-you-type. Use uncontrolled inputs when you only need the value at submit time and want to avoid a re-render on every keystroke. File inputs are always uncontrolled, since browsers do not let scripts set their value.
How do I handle a checkbox in a shared change handler? Checkboxes use the checked prop rather than value, and the new state comes from e.target.checked rather than e.target.value. In a shared handler, look at e.target.type and pick the right property, then write it into your form object with a computed key. Radio buttons share one name and each gets checked set by comparing state to that button's value.
Do I still need server-side validation? Yes, always. Client-side validation is a convenience for honest users; it does nothing against anyone who calls your API directly with curl or edits the request in devtools. Treat the browser check as user experience and the server check as security. The server should reject bad data with a clear message that your form can display next to the offending field.
Is there a built-in React hook for form submission? Recent versions of React added form actions and a hook for tracking submission state, which removes some of the manual submitting flag and error plumbing. Availability depends on your React version and whether you are using a framework that supports server actions, so check what your project is on before adopting them. The controlled-input patterns described here work on every version.