Skip to content

fix(ios): ignore the event emitter attribute when comparing TextInput text - #59718

Open
WataruShirako wants to merge 2 commits into
react:mainfrom
WataruShirako:fix/ios-textinput-ignore-event-emitter-attribute
Open

WataruShirako wants to merge 2 commits into
react:mainfrom
WataruShirako:fix/ios-textinput-ignore-event-emitter-attribute

Conversation

@WataruShirako

@WataruShirako WataruShirako commented Oct 31, 2026 •

Copy link
Copy Markdown

Summary:

Related to #58807 and #58904; part of the CJK IME problems collected in #56463 / #48497.

-[RCTTextInputComponentView _textOf:equals:] calls RCTIsAttributedStringEffectivelySame to decide whether the text JS sent back in updateState: must be written into the field with setAttributedText:. While the user is typing, that write resets UIKit's input pipeline: it cancels the composition of IMEs that do not use marked text (Simeji in #58807, where the field has no markedTextRange so the bare-text fallback does not apply) and drops the keys in flight (#58904).

The comparison treated RCTAttributedStringEventEmitterKey like any other attribute. The text a state is built from carries it on every fragment (RCTNSAttributedStringFromAttributedString adds the parent shadow view's emitter), while text typed into the field does not always carry it (UIKit builds typed text from the field's typing attributes, which are not guaranteed to keep a custom attribute across edits). The reporter's native instrumentation in #58807 shows exactly this (key=EventEmitter incomingRaw=<present> currentRaw=(null)) right before the composition is interrupted. Present on one side and absent on the other, the attribute makes identical text compare different, and the field is rewritten.

The attribute is React-internal metadata: its only reader is RCTTextLayoutManager's hit testing for <Text> press events, and a TextInput dispatches its events through its own emitter, not through the attribute. RCTIsAttributedStringEffectivelySame has a single caller (_textOf:equals:), so this change only affects whether a TextInput rewrites text it already shows. The attribute is now ignored in the comparison, whether it differs or is missing on one side.

Scope relative to the other open work on this path:

Changelog:

[IOS] [FIXED] - Stop rewriting a focused TextInput's text because of the internal event emitter attribute, which interrupted IME composition and dropped keys

Test Plan:

Added three cases to RCTAttributedTextUtilsTest:

React/Tests is not compiled by any open source target, so I added the file to RNTesterUnitTests locally (not part of this change) and ran it on an iPhone 17 Pro simulator, iOS 26.5, with RNTester built from source (RCT_USE_PREBUILT_RNCORE=0). main currently has an unrelated compile failure in RNTesterUnitTests/RCTImageLoaderTests.m (RCTDefineImageURLLoader / RCTDefineImageDecoder are no longer defined anywhere in the tree), so that file was excluded from the local target for these runs.

  • without the fix: Executed 27 tests, with 4 failures: testSamenessOfMissingEventEmitter and testSamenessOfDifferentEventEmitters fail (both assertions each); testDifferenceOfOtherAttributesNextToEventEmitter and the 24 existing tests pass
  • with the fix: Executed 27 tests, with 0 failures, ** TEST SUCCEEDED **

Manual verification in RNTester (Debug, new architecture, same simulator, hardware keyboard disconnected so the software IME is used), with this Playground: a controlled single-line TextInput, a controlled multiline one, a controlled one with maxLength={5} and an uncontrolled one, plus a counter of onChangeText calls.

  • Japanese romaji: the composition underline shows on every composition, space converts, return commits, in all four inputs; the value label and the counter follow the input
  • maxLength={5}: 日本語 commits fine; Hello world typed fast is cut to Hello
  • Chinese Pinyin: zh underlined while composing, 中国 chosen from the candidates
  • Korean: syllables build correctly in the multiline input
  • English: Hello world typed quickly in the controlled single-line input, no key dropped

Recording (iOS 26.5, iPhone 17 Pro simulator, with the fix): ime-after-ios26.5-sim.mp4 (branch evidence/ios-ime-event-emitter on my fork).

one frame every 10 seconds of the recording

Simeji cannot be installed on a simulator, so the #58807 scenario itself (an IME that does not use marked text, with the event emitter missing from the typed text) is covered by the unit test and by the reporter's instrumentation rather than by this recording. I will ask the reporter to confirm on a device.

RNTesterPlayground.js used for the recording
function Field({label, children}) {
  return (
    <View style={styles.field}>
      <RNTesterText style={styles.label}>{label}</RNTesterText>
      {children}
    </View>
  );
}

function Playground() {
  const [single, setSingle] = useState('');
  const [multi, setMulti] = useState('');
  const [limited, setLimited] = useState('');
  const [changes, setChanges] = useState(0);
  const track = set => t => {
    set(t);
    setChanges(c => c + 1);
  };
  return (
    <View style={styles.container}>
      <RNTesterText>onChangeText calls: {changes}</RNTesterText>
      <Field label={`1. controlled single-line (value=${JSON.stringify(single)})`}>
        <TextInput style={styles.input} value={single} onChangeText={track(setSingle)} placeholder="日本語を入力" />
      </Field>
      <Field label={`2. controlled multiline (value=${JSON.stringify(multi)})`}>
        <TextInput style={[styles.input, styles.multiline]} multiline value={multi} onChangeText={track(setMulti)} placeholder="日本語を入力(複数行)" />
      </Field>
      <Field label={`3. controlled maxLength=5 (value=${JSON.stringify(limited)})`}>
        <TextInput style={styles.input} maxLength={5} value={limited} onChangeText={track(setLimited)} placeholder="5文字まで" />
      </Field>
      <Field label="4. uncontrolled single-line">
        <TextInput style={styles.input} placeholder="uncontrolled" />
      </Field>
    </View>
  );
}

`RCTIsAttributedStringEffectivelySame` decides whether
`RCTTextInputComponentView` writes the text JS sent back into the focused
field. It compared `RCTAttributedStringEventEmitterKey` like any other
attribute, but its value is an NSData holding the address of a heap-allocated
weak_ptr, so two wrappers of the same emitter never compare equal, and text
typed into the field may not carry the attribute at all. The comparison then
reported a difference for identical text and the field was rewritten while
the user was typing, which cancels the composition of IMEs that do not use
marked text (react#58807) and drops keys typed during the write (react#58904).

The attribute is React-internal metadata only read by Paragraph hit
testing; a TextInput never dispatches through it, so it is now ignored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@meta-cla

meta-cla Bot commented Oct 31, 2026

Copy link
Copy Markdown

Hi @WataruShirako!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

…comment

Two wrappers of the same emitter do compare equal (the NSData holds the
pointee address), so the earlier wording was wrong; the mismatch is the
attribute present on the state's text and absent from the typed text, which
the test without the fix now shows. The second test uses two distinct
emitters to document that even a different emitter is not a difference for
a TextInput.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 31, 2026
@WataruShirako
WataruShirako marked this pull request as ready for review October 31, 2026 14:05
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 31, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

⌂ Proxy