Skip to main content

Debugging Form Input and Binding Issues with AI Coding Agents

"I type in the field and nothing happens." "The cursor jumps to the end." "The form resets when I tab out." "It loses the last character when I type fast." Input bugs generate the least useful bug reports in front-end development, because the symptom is a feeling — lag, jumpiness, lost keystrokes — and the cause is a binding problem three layers away: a controlled component whose value flips to undefined, a parent re-mounting the field on every render, a validation effect fighting the user for the cursor.

These bugs are brutal for an AI coding agent to fix from a description alone. "Typing feels laggy" matches a dozen causes. What the agent needs is the ground truth of what the user actually did — every keystroke, focus change, and submit, with timestamps — lined up against what the DOM actually did in response. That correlation is the diagnosis, and it is precisely what no code reading can produce.

The Traditional Approach

Reproduce-and-stare: a human types into the field while watching React DevTools highlight renders. Effective, slow, and produces evidence an agent never sees.

Sprinkle onChange logging: works for the handler you instrumented, silent about the parent that re-mounts the input (which is a different bug with identical symptoms — the field loses focus because it is a new element, not because a handler misfired).

Read the component: for controlled-component bugs the code usually looks correct. value={formData.email} — fine, until formData is briefly undefined during a fetch and the input silently flips from controlled to uncontrolled and back, discarding what the user typed in between.

The agnt Approach

Three instruments, all through the proxy, none requiring code changes.

Interaction tracking records what the user did. The injected instrumentation captures click, keydown, input, focus, blur, submit (plus scroll, double-click, context-menu, and a mouse-movement buffer) as structured records with a selector, event type, timestamp, and key details:

proxy {action: "exec", id: "app", code: `
window.__devtool.interactions.getHistory(30)
.filter(i => ['keydown','input','focus','blur','submit'].includes(i.event_type))
`}

Mutation tracking records what the DOM did in response — the other half of the correlation, down to a triggered_by field on each record naming the interaction that provoked it. When keystrokes arrive and the field's subtree shows a remove-and-re-add mutation pattern, the input is being re-mounted, and focus loss is the mechanical consequence.

Framework diagnostics catch the binding bugs React announces itself. The controlled/uncontrolled flip produces a specific console warning, and the currentpage triage classifies it with the fix and the trap:

currentpage {action: "get", proxy_id: "app"}
{
"framework_diagnostics": [
{
"category": "controlled-uncontrolled",
"framework": "react",
"count": 3,
"sample": "Warning: A component is changing an uncontrolled input to be controlled...",
"fix": "The input's value flips between undefined and a defined value. Initialize the state to '' (or the proper empty value) so value is always defined.",
"avoid": "Do not remount with a key or switch to defaultValue — keep a single, always-defined controlled value."
}
]
}

The avoid line is there because remount-with-a-key is the top search result for this warning, and it "works" by destroying the user's in-progress input.

Walkthrough: The Field That Eats Fast Typing

A profile form loads existing data, then lets the user edit:

function ProfileForm() {
const [profile, setProfile] = useState(); // undefined until fetch resolves

useEffect(() => { fetchProfile().then(setProfile); }, []);

return <input
value={profile?.email} // undefined on first renders
onChange={e => setProfile({...profile, email: e.target.value})}
/>;
}

A user who starts typing before the fetch resolves types into an uncontrolled input; when profile arrives, the value snaps to the fetched email and their keystrokes vanish. On a fast connection nobody ever sees it. On a slow one, the bug report says "the form deleted my email."

1. Capture the sequence. Ask the user (or a chaos-injected slow network, which makes this reproducible on demand) to trigger it once, then pull the typed history against the field's surviving value in one call:

proxy {action: "exec", id: "app", code: `
({
typed: window.__devtool.interactions.getHistory(50)
.filter(i => i.event_type === 'input' && i.target && i.target.selector.includes('email'))
.map(i => i.timestamp),
surviving: document.querySelector('input[name=email]').value,
remounts: window.__devtool.mutations.getAdded(Date.now() - 30000)
.concat(window.__devtool.mutations.getRemoved(Date.now() - 30000))
.filter(m => m.target && m.target.selector && (m.added || m.removed))
.map(m => ({type: m.mutation_type, target: m.target.selector, triggered_by: m.triggered_by}))
})
`}

The interaction history shows nine input events on input[name=email]; the field's surviving value is the fetched email, containing none of what was typed. Typed nine, kept zero — the correlation states the bug precisely. (One subtlety worth knowing: a controlled input's typed text updates the value property, which MutationObserver cannot see, so the field's live value is read directly rather than from mutation records. The mutation history's job here is the remounts list — an empty one rules out the re-mounting-parent lookalike, and each mutation record's triggered_by field names the interaction that provoked it.)

2. Check the diagnostics. The controlled-uncontrolled entry above confirms the mechanism — the value went undefined → defined — and rules out the lookalike causes (re-mounting parent, focus-stealing validation) without reading any more code.

3. Fix per the fix field: the value must never be undefined.

const [profile, setProfile] = useState({ email: '' });

Or gate the render on loaded data if an empty flash is unacceptable. What you do not do is the avoid list: defaultValue makes the data loss permanent by design, and key={loaded} remounts the field and drops focus.

4. Verify with the same correlation. Repeat the reproduction; the pull should now show nine input events and nine value mutations, last-writer the user. For the slow-network case that made the bug visible in the first place, keep it visible: the chaos guide shows how to inject the latency deliberately so the regression test doesn't depend on a bad connection.

The same correlation settles the neighboring complaints. "Cursor jumps to the end" — each input interaction is followed by the field's value differing from what the keystroke alone would produce, the signature of a formatter effect rewriting the whole value. "Form resets on tab" — the blur interaction lines up with a subtree remove/re-add in the mutation history (triggered_by points straight at the blur), so a parent is re-rendering on blur-driven validation state. "Submit does nothing" — a submit interaction with no following network entry in proxylog means the handler prevented default and died before the fetch; an entry with a 422 means the server rejected it, and the incident inbox already has the response body.

See Also