Skip to content

Fast typing / held Backspace causes reverted or duplicated characters (setTimeout(0) selection-tracking race) #59

Description

@irina-pr

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions