facebook/react-native

Can't modify text in TextInput onChangeText callback on Android

Opened this issue ยท 71 comments

๐Ÿ› Bug Report

On Android, modifying the text within the onChange (or onChangeText) callback causes corruption of the text in the TextInput. (Not tested on iOS.)

For example, I'm trying to force all caps in my TextInput field. (This is to work around the react native autoCapitalize issue described here: #8932). So if a lowercase letter is entered, I change it to uppercase in the callback. Unfortunately, alternate keystrokes cause the entire previous text to be duplicated, but only if the entered keystroke was lowercase.

So, when forcing all caps, entering 1234 results in 1234 showing up; entering ABCD results in ABCD showing up; but entering abcd results in AABCAABCD.

This issue disappears if assigning a Math.random() key to the TextInput; but then of course so does the keyboard focus, making this an unacceptable workaround.

To Reproduce

See "Bug Report" and "Code Example" sections.

Expected Behavior

One should be able to modify the value inside TextInput's change callbacks, without the text becoming corrupted on the subsequent redisplay.

Code Example

export default class TestScr extends Component
{
  constructor(props)
  {
	super(props);
	this.state = { s6: '' };
  }
  textchg(event)
  {
	const {eventCount, target, text} = event.nativeEvent;
            // one would expect the contents of s6 to display after the redraw
	this.setState({ s6: text.toUpperCase() }); 
  }
  render()
  {
            // [same behavior if using onChangeText instead of onChange]
	let jsx0 = <View style={{ flexDirection: 'row' }} key={ 'hi' }>
		<TextInput placeholder={ 'hello' } value={ this.state.s6 }
			onChange={ (evt) => this.textchg(evt) }
			keyboardType={ 'default' } />
		</View>;
		
	return (<View style={{ backgroundColor: '#ffffff', padding: 10, }}>
		<ScrollView style={{ backgroundColor: '#ffffff', }}>
			{ jsx0 }
		</ScrollView>
	</View>);
  }
}

Environment

React Native Environment Info:
System:
OS: Linux 3.19 Ubuntu 14.04.3 LTS, Trusty Tahr
CPU: (4) x64 Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
Memory: 626.14 MB / 15.38 GB
Shell: 6.18.01 - /bin/tcsh
Binaries:
Node: 8.11.3 - /usr/bin/node
npm: 5.6.0 - /usr/bin/npm
SDKs:
Android SDK:
API Levels: 10, 16, 23, 26, 27, 28
Build Tools: 19.1.0, 20.0.0, 21.1.2, 22.0.1, 23.0.1, 23.0.2, 26.0.3, 27.0.3, 28.0.2, 28.0.3
System Images: android-16 | ARM EABI v7a, android-23 | Intel x86 Atom_64, android-23 | Google APIs Intel x86 Atom_64, android-28 | Google APIs Intel x86 Atom
npmPackages:
react: 16.6.3 => 16.6.3
react-native: 0.58.6 => 0.58.6
npmGlobalPackages:
create-react-native-app: 1.0.0
react-native-cli: 2.0.1

It looks like you are using an older version of React Native. Please update to the latest release, v0.58 and verify if the issue still exists.

The "Resolution: Old Version" label will be removed automatically once you edit your original post with the results of running react-native info on a project using the latest release.

Shouting into the wind! Hope the next person can use this as a basis for reporting the issue.

Sorry about that, but it's really important to make sure the issue is present in the latest release. We've had plenty of issues reported for bugs that have been fixed already.

For bug reports that have a minimal repro example, verifying on the latest version using a brand new project should not take a lot of effort.

Still happening on latest release. I've updated the react-native info text in the description.

@rogerbright is this happening in emulator, or on device?

Tested just now on both an emulator (Nexus 5X API 28) and an actual device (Galaxy Tab A SM-T580). Same results on both.

Sure.

https://snack.expo.io/By1LO7uUV

In the snack interface, the bug appears only on Android. Works correctly on iOS. Make sure you enter only lowercase letters.

@rogerbright ty I will take a look at this!

I am facing the same issue.
I have explain the isuue here #23663 (comment)

@umair-khanzada your issue seems different. In my case, the callbacks are being called just fine... it's what happens when I modify the text that makes things screwy.

Thanks @ericlewis for volunteering to fix it! I think this is really annoying and severe issue, but fortunately, it's not affecting many of our developers.

I am going to label this "mid-pri" while we are waiting for the PR.

this happens with setNativeProps directly as well: #24409

Can confirm this also happens on RN 0.59 as well. Looks like I didn't need to modify controlled text input in a while, but when I did I remember using an overlay over transparent uncontrolled text input that would display the transformed text. I'd say it's pretty serious bug (and pretty old, happens at least since 0.57).

here's a video of a repro: https://streamable.com/j4s4r

and the code (i tested this on 0.57 and 0.59):

import * as React from 'react';
import { Text, TextInput, View, StyleSheet } from 'react-native';

class ControlledInput extends React.Component {
  state = {
    value: '',
  };

  _handleChangeText = ({ nativeEvent: { text } }) => {
    this.setState({ value: text.toUpperCase() });
  };

  render() {
    return (
      <TextInput
        style={{
          width: 300,
          height: 50,
          padding: 10,
          backgroundColor: '#fff',
          borderWidth: 1,
          borderColor: '#eee',
          borderRadius: 2,
        }}
        onChange={this._handleChangeText}
        value={this.state.value}
      />
    );
  }
}

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <ControlledInput />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
});

and a snack: https://snack.expo.io/@notbrent/ranting-sandwich

Any ideas for a workaround?

You could mess around with a Math.random() key for the input field. This blurs the focus after each keystroke though, so you'd have to write code to re-focus and move the cursor to the end. Personally, I no longer modify the input at all on Android, and instead just do the all-caps (or whatever) on the back end before saving to the DB. Not the best user experience. It's too bad the RN team seems uninterested in fixing pretty major issues like this one, and instead seems to spend their time making breaking changes to APIs, moving things arbitrarily out of the core APIs, etc.

method: <TextInput autoCorrect={false} />

@slavikdenis @rogerbright @thymikee @umair-khanzada
I have two different solutions. They aren't exactly adequate for all scenarios. Text selection being one of them. But hopefully they will be of some use:
https://snack.expo.io/@509dave16/android-text-input-out-of-sync-solutions

A text input component that displays a mask over the actual text input.
And a text input that extracts the correct value from the onChangeText, which is providing an out of sync value from the native side.

@hramos I have created a new more up to date issue on this. If you would like for de-duplication purposes, I could move all of the content from this #26017 over here to a comment. Let me know what you think.

My report of this bug

Summary

When attempting to ignore certain characters by controlling the state value that's passed to the TextInput, the TextInput's natively held value get's out of sync. This makes it impossible to enforce a particular text pattern at the time of a user entering the text. This exact issue was occurring before React Native 57.1 on iOS, as noted here in this now closed issue #18874 .

React Native version: 0.59.8(based on Expo SDK 34)

Steps To Reproduce

  1. Implement an onChangeText event handler that will selectively choose to not update the state value(which control's the TextInput's natively stored value). For example, let's say uppercase letters like 'A', 'B', 'C', etc...
  2. Enter characters other than the ones to be ignored
  3. Enter at least 1 or more of the ignored characters
  4. Go back to entering characters other than the ones to be ignored

Describe what you expected to happen:

  1. Enter 'abc', resulting in 'a', 'ab', and 'abc' being passed to onChangeText handler each of which are valid and are set on the state value that controls the TextInput.
  2. Enter 'A' which should be ignored, thus 'abcA' will not be set on the state value that controls the TextInput. Meaning only 'abc' is still displayed in the TextInput.
  3. Enter 'd', which should result in onChangeText handler receiving 'abcd' which is valid and will be set on the state value that controls the TextInput.

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

  1. Snack here
  2. Screenshots that follow the steps outlined in the Describe what you expected to happen section above. Notice after entering 'A' that a pattern emerges in the natively held text value. It concatenates the characters that have been entered to the state controlled value that has remained at 'abc' since the character 'A' was entered.
    • Enter 'a'
    • Enter 'b'
    • Enter 'c'
    • Enter 'A'
    • Enter 'b'

Exists in react-native 0.59.10 too.

I am not sure if this would also work on IOS, but it solved the problem for me on Android. The answer is quite simple: NEVER use value when rendering a TextInput. Use defaultValue instead.

If this turns out to really fix the issue for every platform, maybe we could just issue a warning every time someone uses value as a prop to TextInput.

UPDATE: I've tried this in iOS, and it also works.

@lgenzelis, thanks! So what value is good for again?

stale commented

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

Aetf commented

This is still affecting me.

yeah this is still an issue as far as i know

defaultValue doesn't solve the problem. If you want to mask user inputs and show user masked inputs, still have to use value prop.

Still an issue.

Still seeing this over here as well.

This issue still persists. This is a pretty serious issue for inputting text and there is no special workaround except make a specific event triggering component (like a button) to ensure the textchange happens correctly. Using onEndEditing and oneSubmitHandler also doesn't do anything. Please correct this soon.

Still present in "v0.63.2" which was released couple days ago.
@hramos

Came across the same issue. I wanted to replace the Arabic numerals with English numerals. As @rogerbright mentioned, I ended up using Math.random() key to TextInput (unacceptable workaround though). In my case, users rarely types Arabic numerals.
The below code replaces ู  with 0 and ูก with 2 and so on.

import React, { useState, memo, useRef } from 'react';
import { TextInput } from 'react-native';

//TEST
// const mapping = {
//     '0': 'A',
//     '1': 'B',
//     '2': 'C',
// };

 const mapping = {
     'ู ': '0',
     'ูก': '1',
 }

const updateValue = (val) => {
    if (!val) return val;
    const keys = Object.keys(mapping);
    const regexExpr = new RegExp(keys.join('|'), 'gi');
    if (val.match(regexExpr)) return val.replace(regexExpr, (matched) => mapping[matched]);
    return val;
};

export default memo((props) => {
    const [key, setNewKey] = useState(null);
    const refContainer = useRef(null);
    const onChangeText = (val) => {
        if (props.onChangeText) {
            let newVal = updateValue(val);
            if (newVal != val) {
                setNewKey(`key_${Math.random()}`);
                setTimeout(() => {
                    refContainer.current.focus();
                }, 50);
            }
            props.onChangeText(newVal);
        }
    };
    let randKeys = {};
    if (key) randKeys = { key };
    return <TextInput {...randKeys} {...props} ref={refContainer} value={props.value} onChangeText={onChangeText} />;
});

Here, before it becomes stale.

https://snack.expo.io/@anchi/8017c6

PS: run this on a real device.

Experience the same bug right now. Basically there is no way to implement something as simple as masked input.

Same here. Experiencing this with react-native-masked-input.

mdccg commented

(๏ฝกโ€ขฬ๏ธฟโ€ขฬ€๏ฝก)

same here on version "v0.63.2"

This problem was happening with my react-native App too. What I did is that I created an individual components outside the main component( where we will place this component) keeping TextInput as a whole(i.e according to our requirement that means what field do we need) in one single component. Passing the value as props to this component(if required) and changing it whenever we require using onChangeText. .The problem was solved in my App.

Hello ReactNative Developers! ๐Ÿ˜ƒ

I just prepared Pull Request #29070 that seems to solve this issue

PREVIEWS OF THE BUGFIX

BEFORE AFTER

You can contact me by email at fabrizio.developer@gmail.com

Please head over to the Pull Request #29070 and thumbs up if you like it.
If you want to get this fix you can follow the instructions for building ReactAndroid or checkout my video introduction on forking react-native (patch-package does not work).

If you don't like the pr please feel free to leave a code review or comment, I'll be happy to add improvements and changes.

Thanks a lot ๐Ÿ™ โ˜ฎ๏ธ ๐Ÿ–๏ธ

@fabriziobertoglio1987 You are awesome! Thank you so much.

What I got to work on Android while @fabriziobertoglio1987's PR is open. I want text input to be uppercased. autoCapitalize='characters' works on iOS but there needs to be some additional properties so android doesn't jumble keystrokes

<TextInput
                autoCorrect={false}
                autoCapitalize={Platform.OS === 'ios' ? 'characters' : undefined}
                secureTextEntry={Platform.OS === 'ios' ? false : true}   // Add this for android to prevent duplicate keystrokes
                keyboardType={Platform.OS === 'ios' ? undefined : 'visible-password'}  // Add this for android to prevent duplicate keystrokes
                defaultValue={code}  // Use `defaultValue` instead of `value`
                onChangeText={(text) => setCode(text.toUpperCase()) }
                style={styles.textInputLarge}
                placeholder="enter code"
                returnKeyType="send"
                textContentType="oneTimeCode"
                maxLength={6}
              />

I'm having the same issue having to "format" the user input into an uppercase format. In my case the problem was solved using autoCorrect={false}.

It was indeed a very weird problem, very hard to debug and hard to search for (I found this thread after quite some researching - probably I need to work on my google ability).

Thank you @fabriziobertoglio1987 for working on a fix.

Please, is it possible to merge this PR in the next version ?

To answer the question of when the pr #29070 which fixes this issue will be released in the next version of React Native, I suggest you to follow the discussions at https://github.com/react-native-community/releases/issues by subscribing to those threads. The process of releasing a new version with a bug fix is the following:

  • an Issue is opened in facebook/react-native
  • a contributor writes a Pull Request to fix/close the react-native Issue
  • The Pull Request is merged in Master
  • For each realease (the upcoming one is 0.64.1) there will be a separate discussion on https://github.com/react-native-community/releases/issues where anyone can request a commit to be cherrypicked from master. The link need to be from the a commit from master (for ex. #29157 (comment)) and not a Pull Request not yet merged in master โ˜ฎ๏ธ ๐Ÿ™

Same here on version "v0.68.1" in both platforms

FWIW, I did end up solving this issue and many others. The solution starts with an "F" and rhymes with "butter." I realize that's not an option for everyone.

FWIW, I did end up solving this issue and many others. The solution starts with an "F" and rhymes with "butter." I realize that's not an option for everyone.
Flutter doens't have this issue, right?

Correct. I'm only six months in but so far my impression is that Flutter/Dart is mature, well thought-out, concise, and free of sanctimony.

I look again into this when I have more free time. I need to update my PR with the improvements included in #33468. For this reason #29070 is back to draft. We can not trigger setText in a ReactEditText. replace is used on purpose. Sorry for the delay in the development of this fix.

Hey guys, any news on this one?

I guess not.

+1

I'm seeing this on iOS but I suspect it's because of one of the packages I have installed.

  "dependencies": {
    "@react-navigation/bottom-tabs": "^6.5.3",
    "@react-navigation/native": "^6.1.2",
    "@react-navigation/native-stack": "^6.9.8",
    "expo": "^47.0.13",
    "expo-splash-screen": "~0.17.5",
    "expo-status-bar": "^1.4.2",
    "firebase": "^9.15.0",
    "immer": "^9.0.17",
    "jest": "^29.3.1",
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "react-native": "0.70.5",
    "react-native-dotenv": "^3.4.7",
    "react-native-paper": "^5.1.3",
    "react-native-plaid-link-sdk": "^8.0.1",
    "react-native-safe-area-context": "^4.4.1",
    "react-native-screens": "^3.18.2",
    "react-native-text-input-mask": "^3.1.4",
    "react-native-web": "^0.18.10",
    "zustand": "^4.3.2"
  },
  "devDependencies": {
    "@babel/core": "^7.20.12",
    "@types/react": "^18.0.26",
    "@types/react-native": "^0.71.0",
    "babel-plugin-module-resolver": "^5.0.0",
    "babel-plugin-root-import": "^6.6.0",
    "expo-cli": "^6.1.0",
    "typescript": "^4.9.4"
  },

@chancegraff I have the same issue on iOS, were you able to fix this?

jcubic commented

The same issue on Android in Emulator. It works fine for the Expo playground so don't use it to test.

Simple reproduction:

const ControlledComponent = () => {
  const [state, setState] = useState('0.00');
  const cursor = 1;
  return (
    <TextInput
      ref={ref}
      autoFocus
      onKeyPress={() => { /* empty */ }}
      selection={{start: cursor, end: cursor}}
      value={state}
      keyboardType="numeric"
    />
  );
};

There is no way to prevent default behavior, event.preventDefault() also doesn't work. The input is always updated even if the component is controlled.

The issue still persists on both IOS and Android

I try to limit decimal points to 8 length. The user sees 9th character shortly before removal. It causes flickery.

https://snack.expo.dev/N5IcDRFWc

Do you still experience this issue?

I have four years of experience maintaining facebook/react-native and I specialize in the Text and TextInput components. I currently have 58 facebook/react-native PRs.

If you still experience this issue, I will prepare a patched release with the fix.

Thanks a lot

Yes, this bug still occurs. Please take care of it

@fabOnReact
Yes, the issue still persists (on iOS). If you could tackle this, it would be incredibly appreciated!

Do you still experience this issue?

I have four years of experience maintaining facebook/react-native and I specialize in the Text and TextInput components. I currently have 58 facebook/react-native PRs.

If you still experience this issue, I will prepare a patched release with the fix.

Thanks a lot

Kind of a hacky work around (and doesn't always work quite as expected):

const onChange = (e: string) => {
    if (isValidInput(e)) {
        textInputRef.current?.setNativeProps({ text: e });
    }
}

Kind of a hacky work around (and doesn't always work quite as expected):

const onChange = (e: string) => {
    if (isValidInput(e)) {
        textInputRef.current?.setNativeProps({ text: e });
    }
}

Thank you! I already tried that, but It's still flickering on device... It works fine on Simulator.
Incidentally, I would like to prevent type new lines.

Kind of a hacky work around (and doesn't always work quite as expected):

const onChange = (e: string) => {
    if (isValidInput(e)) {
        textInputRef.current?.setNativeProps({ text: e });
    }
}

Thank you! I already tried that, but It's still flickering on device... It works fine on Simulator. Incidentally, I would like to prevent type new lines.

Can you try something like this?

. . .
const previousValueRef = React.useRef('');
. . .
const onChange = (e: string) => {
    if (textInputRef.current) {
        const textInput = textInputRef.current;
        textInput.setNativeProps({ text: previousValueRef.current });
        if (isValidInput(e)) {
            textInput.setNativeProps({ text: e });
            previousValueRef.current = e;
        }
    }
}
. . .

If you need to accept only integers and know your max length, there was a suggestion from this SO post:

<TextInput
  maxLength={MAX_LENGTH}
  keyboardType="numeric"
  value={value}
  onChangeText={(e) => {
    if (/^\d+$/.test(e)) setValue(e);
  }}
  placeholder="Type only numbers..."
/>

This PR is included in the react-native-improved library:

react-native-improved

  • Supports ONLY react-native 0.73.
  • Supports only old architechture (new architechture is WIP)

Set-up

In package.json

 "scripts": {
+  "postinstall": "yarn run react-native-patch"
 }

Then

npm

npm install react-native-improved --save-dev

yarn v1

yarn add react-native-improved --dev

Can you try something like this?

Thank you!
This did not work out well either. However, it might have been due to me including a Text component as a child of TextInput to finely control the style :

<TextInput ... >
  <Text>Some</Text>
  <Text style={{ ... }}>String</Text>
</TextInput>

For my case, I've decided to use a custom Native Module, but I'll give your solution a try another time.

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

A PR with a fix was created a year ago, I don't know what else I can do.

A PR with a fix was created a year ago, I don't know what else I can do.

What's the PR?

It was linked:

#38155

It was linked:

#38155

That's an issue, not a PR

That's an issue, not a PR

Ah sorry my mistake, I thought it was the issue where I created a PR for.