Back to the Library

React Input Not Working - Why You Can't Type Anything

4 min read
“You click into the field. You type. Nothing appears. The input looks frozen - and the reason is that you told React to control it, then never told React what to do when it changes.”

In This Post

The Controlled Input ContractThe Missing HalfThe Fix: onChangeThe Other Failure: defaultValueThe Fix: Switch to value + onChangeA Quick Way to Diagnose Which One You HavePractice These Patterns

Practice This Pattern

White Belt

Frozen Input

A name input that takes a value prop but no onChange - every keystroke is rejected and the field never shows what you type.

+10 KI
Enter the Dojo
Blue Belt

Uncontrolled Input Ignores Reset

A name editor using defaultValue instead of value - the saved label updates correctly but the input field itself never reflects a Reset.

+25 KI
Enter the Dojo
BugDojo
BlogFAQ

© 2026. Carved in code.

You click into the field. You type. Nothing appears.

The cursor blinks. The key presses register - you can feel them, maybe even see a console.log fire. But the character never shows up in the box.

This is not a typing problem. It is a value problem.

The Controlled Input Contract

<input value={name} placeholder="Type your name" />

The moment you write value={name}, you have told React: "this input always shows exactly what name holds. Nothing else."

React takes that seriously. On every keystroke, the browser tries to update the input's displayed text. React steps in and overwrites it back to name - because that is what you asked for. If name never changes, the input snaps back to the same value every single time, instantly.

It is not broken. It is doing exactly what you told it to do, perfectly, forever.

The Missing Half

export default function NameInput() {
  const [name, setName] = useState("");
 
  return (
    <div>
      <input value={name} placeholder="Type your name" />
      <p>Hello, {name || "stranger"}!</p>
    </div>
  );
}

value={name} is only half of a controlled input. The other half is the part that updates name when the user types. Without it, name is stuck at "" forever, and so is the input.

Symptom

The cursor moves. The field accepts focus. Nothing else happens. No error, no warning in most setups - just a box that silently refuses every character.

The Fix: onChange

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

Now the loop closes. User types a character. The browser fires onChange. setName(e.target.value) updates state to the new text. React re-renders. value={name} now equals the new text. The input shows it.

Every keystroke runs this full loop. It feels instant because it is - but it only works because both halves of the contract are there.

The Other Failure: defaultValue

There is a second way to break this, and it looks completely different on screen.

export default function NameEditor() {
  const [name, setName] = useState("Alice");
 
  return (
    <div>
      <input
        defaultValue={name}
        onBlur={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
      <p>Saved: {name}</p>
      <button onClick={() => setName("Alice")}>Reset</button>
    </div>
  );
}

This one is not frozen. Typing works fine. Click Reset and the "Saved:" label correctly flips back to "Alice" - but the input field itself still shows whatever you last typed.

The Sensei's Hint

defaultValue sets the input's starting value once, at mount, and then React stops touching it. The input becomes uncontrolled - it manages its own text from then on, completely independent of state.

That is why Reset half-works. setName("Alice") updates state, the label re-renders correctly, but the input was never wired to name in the first place. There is nothing for the new state value to flow into.

The Fix: Switch to value + onChange

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

Now the input is controlled the same way as the first example. When Reset calls setName("Alice"), the value prop becomes "Alice" on the next render, and the input field updates along with the label.

Constraints

Pick one model per input and stay there. value + onChange means React drives the field - every change must go through state. defaultValue means the DOM drives the field - React sets it once and never again. Mixing them, or switching halfway through a component's life, is where both of these bugs come from.

A Quick Way to Diagnose Which One You Have

If typing does nothing at all: you have value without onChange. The input is fully controlled and frozen on whatever state currently holds.

If typing works but external updates (resets, props, other buttons) don't reach the field: you have defaultValue where you needed value. The input is uncontrolled and only the DOM knows what it currently shows.

Both are one-line fixes once you know which contract you actually want.

Practice These Patterns

Two katas, two sides of the same contract. "Frozen Input" is the direct version - one missing onChange, one completely unresponsive field, cause and effect are immediate. "Uncontrolled Input Ignores Reset" is the sneakier one, because typing works and the bug only shows up when something else tries to change the field. Do the frozen one first to lock in the mental model, then the reset one to see how it breaks when only half the wiring is missing.