Environment
@react-input/mask: 2.0.4
@react-input/core: 2.0.2
- React: 19.2.7
- Wrapped in a controlled
<input> via react-hook-form's Controller (value/onChange driven by RHF field state)
Bug
With a mask like +________ (literal + followed by digit placeholders, replacement: { _: /\d/ }), typing or deleting characters fast (e.g. holding Backspace, or two keystrokes in quick succession — digit+digit or digit+Delete) intermittently causes previously-deleted characters to reappear, or a keystroke to be silently dropped/duplicated. It's most visible when deleting down to an empty field: the field briefly flashes empty and then previously-typed digits reappear on their own, even though the user only pressed Backspace.
Root cause (as far as I could trace it)
In @react-input/core's Input class, the native "input" event only exposes the post-edit selection. To classify insert/deleteForward/deleteBackward and compute the edited range, the library needs the pre-edit selection, so it caches it via a self-rescheduling window.setTimeout(fn, 0) loop started on focus (roughly):
var tick = () => {
cache.selectionStart = input.selectionStart;
cache.selectionEnd = input.selectionEnd;
cache.id = window.setTimeout(tick);
};
The "input" handler gates on whether that timer has ticked since the last processed event:
if (cache.cachedId === cache.id) throw new SyntheticChangeError("The input selection has not been updated.");
cache.cachedId = cache.id;
The catch block for that error reverts the DOM value to the last cached value and stops the event:
catch (err) {
input.value = cache.value;
input.setSelectionRange(cache.selectionStart, cache.selectionEnd);
event.preventDefault();
event.stopPropagation();
if (err.name !== "SyntheticChangeError") throw err;
}
setTimeout(fn, 0) callbacks are macrotasks, and nested zero-delay timeouts are throttled by browsers to a ~4ms floor. Under native OS key-repeat (events roughly every 20–30ms) — or just two real keystrokes typed quickly — combined with any non-trivial synchronous JS work happening on the same tick (e.g. a form library re-validating on every change), the browser can
Environment
@react-input/mask: 2.0.4@react-input/core: 2.0.2<input>viareact-hook-form'sController(value/onChange driven by RHF field state)Bug
With a mask like
+________(literal+followed by digit placeholders,replacement: { _: /\d/ }), typing or deleting characters fast (e.g. holding Backspace, or two keystrokes in quick succession — digit+digit or digit+Delete) intermittently causes previously-deleted characters to reappear, or a keystroke to be silently dropped/duplicated. It's most visible when deleting down to an empty field: the field briefly flashes empty and then previously-typed digits reappear on their own, even though the user only pressed Backspace.Root cause (as far as I could trace it)
In
@react-input/core'sInputclass, the native"input"event only exposes the post-edit selection. To classifyinsert/deleteForward/deleteBackwardand compute the edited range, the library needs the pre-edit selection, so it caches it via a self-reschedulingwindow.setTimeout(fn, 0)loop started on focus (roughly):The
"input"handler gates on whether that timer has ticked since the last processed event:The catch block for that error reverts the DOM value to the last cached value and stops the event:
setTimeout(fn, 0)callbacks are macrotasks, and nested zero-delay timeouts are throttled by browsers to a ~4ms floor. Under native OS key-repeat (events roughly every 20–30ms) — or just two real keystrokes typed quickly — combined with any non-trivial synchronous JS work happening on the same tick (e.g. a form library re-validating on every change), the browser can