react-native: TextInput onChangeText is called with empty text if maxLength is exceeded on iOS

Description

If the max length of a controlled TextInput is exceeded on iOS onChangeText will get called with an empty string. We noticed this issue when our users added an emoji as the last character in a TextInput. We only see the issue on iOS, on Android the the emoji can not be added. Since emojis are counted as two chars I’m guessing the emoji is causing the text to have length maxLength+1 and that is causing issues on iOS.

Using an uncontrolled TextInput the emoji is replace with the last character in the screenshot below but keeps it’s value. Screenshot 2020-04-28 at 14 48 00

React Native version:

0.61.4

Steps To Reproduce

Provide a detailed list of steps that reproduce the issue.

  1. Create a controlled TextInput with a maxLength
  2. Input text until you reach maxLength - 1
  3. Add a emoji

Expected Results

Same behaviour as on Android that the emoji is not accepted as input.

Snack, code example, screenshot, or link to a repository:

https://snack.expo.io/8x3dBwzJe

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 7
  • Comments: 15

Most upvoted comments

As a workaround I’ve removed maxLength prop and added the following code to the onChangeText callback:

onChangeText={value => {
    if (value.length <= maxLength) {
        setNewValue(value)
    }
}}

It minimizes the chances of the bug happening but doesn’t fix it.

I’ve found a workaround that fixes the issue:

import * as React from 'react'
import { Platform, TextInput, TextInputProps } from 'react-native'

export interface TextInputFixedProps extends TextInputProps {
  onChangeText: (text: string) => void
}

/**
 * Workaround for https://github.com/facebook/react-native/issues/28774
 * and https://github.com/status-im/status-react/issues/12919.
 *
 * Fixes `onChange` clearing the whole input when an emoji is typed as the last
 * character when having `maxLength`. This happens on iOS only.
 *
 * Requires `onChangeText` to be set.
 */
export function TextInputFixed(props: TextInputFixedProps) {
  if (Platform.OS === 'ios') {
    return (
      <TextInput
        {...props}
        onChangeText={undefined}
        onChange={(event) => {
          const newText = event.nativeEvent.text
          const currentText = event._dispatchInstances.memoizedProps.text
          const clearsInput =
            props.maxLength &&
            currentText.length === props.maxLength - 1 &&
            newText.length === 0
          if (!clearsInput) {
            props.onChangeText(newText)
          }
        }}
      />
    )
  } else {
    return <TextInput {...props} />
  }
}

It’s a bit ugly to access the private _dispatchInstances, but it does work.

I’m using React Native ~0.63.4.