Published on

TIL: Synthetic blur and change events break React controlled inputs during automation

TIL: Synthetic blur and change events break React controlled inputs during automation
Authors

A browser automation script kept locking text inputs on a React app built with Material UI. The script assigned the value and dispatched change and blur:

input.value = 'hello'
input.dispatchEvent(new Event('change', { bubbles: true }))
input.dispatchEvent(new Event('blur', { bubbles: true }))

On plain HTML pages this works. On React, the input froze. The text appeared for one frame, then vanished on submit, or the input stopped responding to keystrokes.

Two things broke.

First, React overrides the native value property descriptor with its own tracker. Assigning input.value directly bypasses React tracking. When React processes a native change event, it checks whether the tracked value changed. Because the tracker missed the assignment, React discards the change and reverts the field.

Second, MUI inputs listen to blur to commit validation and shrink floating labels. When the automation script fired blur immediately after change, the blur handler ran before React processed the state update. The component locked focus and discarded the uncommitted input state.

The fix is to call the native setter and dispatch only input:

const prototype = window.HTMLInputElement.prototype
const nativeSet = Object.getOwnPropertyDescriptor(prototype, 'value').set

nativeSet.call(input, 'hello')
input.dispatchEvent(new Event('input', { bubbles: true }))

React listens for the bubbling input event on the document root, updates its tracker, and triggers its own synthetic onChange. If you need direct component access, you can also read the Fiber key memoizedProps.onChange from the DOM node.

When automating React forms, do not dispatch synthetic change or blur events. Use the native prototype setter and dispatch a single bubbling input event.