react-native: TextInput order of events are inconsistent between iOS and Android

TextInput event order are different for iOS and Android.

This inconsistency make certain operations hard to archive on Android:

  • Detect backspace key on empty text input to delete the component. If only 1 character is left in the text, pressing backspace key will delete the text first before we process which key is pressed, which will make us assume that the text input is empty.
  • Processing text on change text event before selection change event can cause crash due to invalid selection.

Environment

Tested on Expo 25.0.0/0.52.0, but original issue happen in 0.53.3 as well, Likely exist in 0.54.0

Expected Behavior

I think iOS order of events make the most sense and I hope Android can follow the suit.

Actual Behavior

The event in question are onChangeText(), onSelectionChange() and onKeyPress().

On iOS, the events are in the following order:

  • key press
  • selection change
  • change text

screenshot 2018-03-06 17 12 21

On Android, the events are in the following order:

  • change text
  • selection change
  • onKeyPress (See #18262)

screenshot 2018-03-06 17 12 27

Steps to Reproduce

https://snack.expo.io/Hk2qtCouf

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 25
  • Comments: 15

Most upvoted comments

same issue, please fix it

FWIW, I made this Snack to help debug the onChangeText/onKeyPress before discovering this open issue: https://snack.expo.io/BJM4Rf9Rf

On Android this logs in the following order:

  • onChange
  • onChangeText
  • onKeyPress

Env:

Environment:
  OS: macOS High Sierra 10.13.4
  Node: 8.9.4
  Yarn: 1.5.1
  npm: 5.6.0
  Watchman: 4.9.0
  Xcode: Xcode 9.2 Build version 9C40b
  Android Studio: 3.1 AI-173.4670197

Packages: (wanted => installed)
  react: 16.3.0-alpha.1 => 16.3.0-alpha.1
  react-native: https://github.com/expo/react-native/archive/sdk-26.0.0.tar.gz => 0.54.2

My workaround is acting like the onKeyPress and onTextChange event but doing so on onChange. I have to manually find the string difference though to determine the “key” they pressed. This will act the SAME across iOS and Android. Demo: https://snack.expo.io/@loonison101/handle-event-firing-discrepancy-rn

onChange({nativeEvent}) {
	const {text: newText} = nativeEvent;
    const {value: oldText, onChange} = this.props; // onChange passed from the parent
    const key = findFirstDifferentChar(newFormattedText, oldText);

    onChange(key, newText);
}
findFirstDifferentChar(currentValue = '', beforeValue = '') {
    if (beforeValue.length > currentValue.length) {
      return 'Backspace'; // They backspaced, same name RN gives the char
    }

    let startIndex;
    [...currentValue].forEach((char, index) => {
      if (startIndex !== undefined) {
        return;
      }

      if (char !== beforeValue[index]) {
        startIndex = index;
      }
    });

    return currentValue[startIndex || 0];
  }
<TextInput value={value} onChange={this.onChange} />

https://github.com/leighman/react-native-text-input-selection-crash for reproduction of crash when setting selection and text at the same time. Works fine on iOS. Crashes on Android unless selection is set in the setTimeout. Exists in 0.52 - 0.54 as far as I can tell.