Skip to main content

Command Palette

Search for a command to run...

Fixing iOS TextInput Cursor Drift in React Native's Legacy Architecture

Updated
5 min readView as Markdown

An input can render correctly and accept keyboard events, yet insert subsequent characters into the middle of a word during fast typing. Investigating that behavior requires following UIKit autocorrection, controlled-value updates, and selection state across their timing boundaries.

I encountered this in an application that still needed the Legacy Architecture. An upstream fix covered the New Architecture. I adapted its approach to Paper's native input implementation, shared an RN 0.77.3 patch publicly, and later maintained the fix during an RN 0.81.5 upgrade.

This case study distinguishes the upstream contribution, my adaptation, and the available validation.

Identify the unnecessary update

Identify the unnecessary update

Technical flow; implementation and validation boundaries are explained below.

Issue #44157 describes cursor drift in controlled inputs with autocorrection and predictive text involved. PR #46970 explicitly addresses the New Architecture. Original issue, upstream fix.

The upstream investigation identified a timing problem: UITextField can update text and selection in separate stages during autocorrection. Replacing its complete attributedText in between can preserve an intermediate caret position.

The useful question was why a controlled-value round trip rewrote the input when the returned content was already effectively equivalent.

Compare effective attributes in Paper

Plain character equality ignores meaningful styling. Strict NSAttributedString equality can also reject equivalent input because UIKit's default typing attributes differ from RN-generated attributes.

Following the upstream approach, I added effective attribute comparison to Paper's RCTBaseTextInputView.mm. The implementation checks string content, compares attributes across character ranges, uses captured initial typingAttributes as a baseline for missing values, and normalizes natural paragraph alignment and writing direction.

Actual content, range, or effective style differences still have an update path. The work required adapting the principle to Paper's objects and lifecycle; Fabric's input component and attribute types could not simply be copied into a different file.

Handle two distinct update boundaries

Single-line inputs

This is the core guard from the RN 0.81.5 adaptation, with surrounding object preparation omitted. Variables belong to the RCTBaseTextInputView update path; textOf:equals: already uses the effective comparison described above. It is an excerpt, not a standalone patch.

UITextRange *selection = self.backedTextInputView.selectedTextRange;
BOOL isSinglelineTextField =
    [self.backedTextInputView isKindOfClass:[UITextField class]];
BOOL stringsMatch =
    [attributedTextCopy.string isEqualToString:backedTextInputViewTextCopy.string];

if (eventLag == 0 &&
    isSinglelineTextField &&
    self.backedTextInputView.isFirstResponder &&
    (selection == nil || selection.empty) &&
    stringsMatch) {
  textNeedsUpdate = NO;
} else {
  textNeedsUpdate =
      ([self textOf:attributedTextCopy
            equals:backedTextInputViewTextCopy] == NO);
}

The review point is the final assignment decision: textNeedsUpdate = NO avoids the subsequent whole-attributedText reset. Other cases still pass through the comparator. It neither forces the caret to the end nor disables autocorrection. The public patch and upstream attribution are linked later in this article.

The legacy patch skips an identical-text rewrite only when all of these conditions hold:

Condition Scope
eventLag == 0 JS and native text event counts agree
The backing control is a UITextField Single-line input
The control is the first responder Active editing
Selection is empty or absent No selected text range
Old and new strings match No new character content to apply

This does not directly detect autocorrection. It protects an identical-text update that may occur while the caret is temporarily stale. Actual text changes retain the normal update path.

The guard is broader than effective attribute comparison: a style-only update during active editing may also skip that complete rewrite. Same-text style updates, selection changes, and composition therefore belong in regression testing. The patch should not be described as having no effect on every input scenario.

Multiline inputs

The patch also introduces a programmatic-update boundary between RCTUITextView and its delegate adapter. Relevant editing callbacks are suppressed during JS-driven writes, and string and selection snapshots are synchronized afterward.

This handles feedback from programmatic writes. It is a separate path from single-line autocorrection and needs separate review and validation.

Maintain the patch across versions

The public patch targets Expo 52 / RN 0.77.3. During the RN 0.81.5 migration, I inspected the legacy implementation again, identified the safeguards still needed, adapted the relevant changes, and regenerated the dependency patch.

Delivery included the native implementation, patch file, and corresponding lockfile identity. Keeping them aligned lets another developer or CI reproduce the modified dependency after installation.

The original patch and my report are available in the upstream discussion. The upstream authors implemented the Fabric fix; my contribution is the Paper adaptation and its subsequent maintenance. My legacy patch and report.

Report the observed result accurately

In that public report, I stated that we had not received further user reports after adopting the patch. The observation did not include a defined period, input count, or control group, so it does not establish a measured fix rate or permanent elimination.

Historical validation of the RN 0.81.5 adaptation included applying the patch against a clean dependency, installation with a frozen lockfile in offline mode, and an iOS Simulator build of the React-RCTText target.

Those checks establish patch reproducibility and native compilation. That validation record did not include real-device fast typing or predictive-keyboard regression. No device tests were rerun while writing this article.

For a similar engagement, I would test rapid typing, autocorrection, the double-space period shortcut, English and Chinese input methods, selection replacement, programmatic clearing, and dynamic styles across single-line and multiline inputs, recording both RN version and architecture.

What the work delivers

An application that cannot immediately switch architectures still needs usable input. This work connects upstream native analysis to a version-specific implementation, reproducible dependency changes, and an explicit validation scope that another maintainer can continue.


ivan provides part-time remote React Native / iOS support, focusing on native debugging, upgrades, and patch maintenance through written, asynchronous collaboration.

Work with me

More from this blog

I

ivan — Mobile Engineering Notes

7 posts

Practical case studies on React Native, Expo upgrades, iOS native debugging, mobile CI/CD, and Maestro E2E. I share code, diagrams, and validation notes from implementation work, with design proposals clearly labeled. Written by ivan, available for part-time technical support through written, asynchronous collaboration.