facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.26k stars 24.34k forks source link

TextInput onChangeText called automatically for maxLength and multiline #36494

Closed shrihari1999 closed 1 year ago

shrihari1999 commented 1 year ago

Description

Consider a TextInput that has maxLength set and multiline true. When the text input's initial value's length is more than half the maxLength value, onChangeText is automatically called on render.

I am unable to reproduce this in a snack, happens on multiple iOS devices I've tried. No issue with android.

React Native Version

0.71.3

Output of npx react-native info

System: OS: Linux 4.15 Ubuntu 16.04.7 LTS (Xenial Xerus) CPU: (4) x64 Intel(R) Core(TM) i5-2320 CPU @ 3.00GHz Memory: 3.66 GB / 7.76 GB Shell: 4.3.48 - /bin/bash Binaries: Node: 16.17.0 - ~/.config/nvm/versions/node/v16.17.0/bin/node Yarn: 1.22.19 - ~/.config/nvm/versions/node/v16.17.0/bin/yarn npm: 8.15.0 - ~/.config/nvm/versions/node/v16.17.0/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman SDKs: Android SDK: API Levels: 28, 29, 30, 31 Build Tools: 28.0.3, 29.0.2, 29.0.3 System Images: android-27 | Google Play Intel x86 Atom Android NDK: Not Found IDEs: Android Studio: Not Found Languages: Java: 1.8.0_251 - /home/shrihari/jdk1.8.0_251/bin/javac npmPackages: @react-native-community/cli: Not Found react: 18.2.0 => 18.2.0 react-native: 0.71.3 => 0.71.3 npmGlobalPackages: react-native: Not Found

Steps to reproduce

  1. Use a TextInput with maxLength={10} and multiline={true}
  2. Set the initial state of textinput's value to a string of length more than 5.
  3. Observe onChangeText is automatically called with wrong text parameter

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

import React, { useState } from 'react'
import { SafeAreaView, TextInput } from 'react-native'

export default function EditProfileFieldScreen({ route }) {
    // const [fieldValue, setFieldValue] = useState('abcde') // works correctly
    const [fieldValue, setFieldValue] = useState('abcdef') // does not work correctly
    console.log('initial length', fieldValue.length)

    const handleTextChange = (text) => {
        console.log('text change called', text.length)
        setFieldValue(text)
    }

    return (
        <SafeAreaView style={{flex: 1, backgroundColor: 'white'}}>
            <TextInput
                value={fieldValue}
                onChangeText={handleTextChange}
                maxLength={10}
                multiline={true}
            />

        </SafeAreaView>
    )
}

I am using Expo SDK 48. I am also running the lastest version of Expo GO

github-actions[bot] commented 1 year ago
:warning: Newer Version of React Native is Available!
:information_source: You are on a supported minor version, but it looks like there's a newer patch available. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.
Fb9027 commented 1 year ago

Ok

shrihari1999 commented 1 year ago

warning Newer Version of React Native is Available! information_source You are on a supported minor version, but it looks like there's a newer patch available. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.

I tried upgrading to the latest version (0.71.4). I can confirm that the issue still persists. Someone please help!

SrikrishnaParthasarathy commented 1 year ago

Hello, has anyone found a solution? I'm having the same problem.

remanation commented 1 year ago

We also experience this. I would be really nice to get fixed.

remanation commented 1 year ago

I found out it was introduces in this commit: https://github.com/facebook/react-native/commit/112bfeecfac393384de99866569b311dd00a6908 I tried to revert the changes locally and that fixed the issue. For now we will apply a patch in our repo the reverts the changes. I'm not really sure why the changes cause the issue. @s77rt could you maybe have a look as it seems like you did the changes.

shrihari1999 commented 1 year ago

In case anyone doesn't want to change the source code, I did a dirty settimeout workaround, that basically shields the component from updating for the first 300ms.

Someone please fix this officially!!

s77rt commented 1 year ago

@remanation I have been looking into this. I think that the bug existed for long but instead of having onChangeText fire it was onSelectionChange. In the new version (0.71) every time onSelectonChange is about to fire, we will first fire onChangeText (to switch the order).

So the real bug to investigate is: Why onSelectionChange fires at render on a multiline TextInput. This is reproducible even on older versions (before the linked commit): https://snack.expo.dev/uy58XKz_S?platform=ios

smhaltnsk commented 1 year ago

0.70.6 not have this bug, I upgraded to 0.71.4 and now I have this bug

jzaleski commented 1 year ago

Just spent this evening chasing down this issue, we're on 0.71.4, and then I stumbled across this issue. Bah. Appreciate you guys looking into this!

jzaleski commented 1 year ago

In case anyone doesn't want to change the source code, I did a dirty settimeout workaround, that basically shields the component from updating for the first 300ms.

Someone please fix this officially!!

As a temporary solution until the root-cause can be addressed I did the the same thing -- thanks for the recommendation @shrihari1999.

shrihari1999 commented 1 year ago

Quick update: Upgrading to 0.71.6 didn't fix it either

rottabonus commented 1 year ago

In case anyone doesn't want to change the source code, I did a dirty settimeout workaround, that basically shields the component from updating for the first 300ms. Someone please fix this officially!!

As a temporary solution until the root-cause can be addressed I did the the same thing -- thanks for the recommendation @shrihari1999.

Hi all, could one of you provide an example of this workaround?

EDIT:

I made myself it like this

type Props = {
  text: string;
  setText: React.Dispatch<React.SetStateAction<string>>;
  maxLength: number;
};

export default (props: Props) => {
//github.com/facebook/react-native/issues/36494
React.useEffect(() => {
    setTimeout(() => props.setText(props.text), 300);
  }, []);

  return (
      <TextInput
        value={props.text}
        onChangeText={props.setText}
        multiline
        maxLength={props.maxLength}
      />
  );
};

The problem with my approach is that the wrong value flashes on the input before the correct one is set.. =P

shrihari1999 commented 1 year ago

@rottabonus The point is to prevent onChangeText from affecting your state variables for 300ms. Try refactoring your code to this.

type Props = {
  text: string;
  setText: React.Dispatch<React.SetStateAction<string>>;
  maxLength: number;
};

export default (props: Props) => {
  const [onChangeShield, setOnChangeShield] = React.useState(true)
  //github.com/facebook/react-native/issues/36494
  React.useEffect(() => {
    if(onChangeShield){
        setTimeout(() => {
            setOnChangeShield(false)
        }, 300);
    }
  }, []);

  const handleTextChange = (text) => {
      if(onChangeShield){
          return
      }

      props.setText(text)
  }

  return (
      <TextInput
        value={props.text}
        onChangeText={handleTextChange}
        multiline
        maxLength={props.maxLength}
      />
  );
};
rottabonus commented 1 year ago

@rottabonus The point is to prevent onChangeText from affecting your state variables for 300ms. Try refactoring your code to this.

type Props = {
  text: string;
  setText: React.Dispatch<React.SetStateAction<string>>;
  maxLength: number;
};

export default (props: Props) => {
  const [onChangeShield, setOnChangeShield] = React.useState(true)
  //github.com/facebook/react-native/issues/36494
  React.useEffect(() => {
    if(onChangeShield){
        setTimeout(() => {
            setOnChangeShield(false)
        }, 300);
    }
  }, []);

  const handleTextChange = (text) => {
      if(onChangeShield){
          return
      }

      props.setText(text)
  }

  return (
      <TextInput
        value={props.text}
        onChangeText={handleTextChange}
        multiline
        maxLength={props.maxLength}
      />
  );
};

Thanks for the suggestion!

It works better than my implementation, but still there is a tiny flash - but one you almost cannot see. That will have to do for now.. :p

Here is a small clip:

https://user-images.githubusercontent.com/34128180/230760368-1026a5ec-0cb6-45f3-a28d-054569352dd8.mov

BTW the application is open sourced, so no worries of me showing something secret =)

dmytrorykun commented 1 year ago

I can confirm that onChangeText (onSelectionChange before https://github.com/facebook/react-native/commit/112bfeecfac393384de99866569b311dd00a6908) is called for every render of multiline TextInput. @shrihari1999 do you confirm that maxLength and text length is relevant here? For me this issue happens regardless of text length.

shrihari1999 commented 1 year ago

Sorry @dmytrorykun. I'm not aware of the onSelectionChange/onChangeText saga.

For me, there is no problem on an iOS device if: a) maxLength is not set b) If initial state value length is less than half of maxLength.

I'm not sure if its relevant to the underlying problem. But, I can confirm that this is my observation on any physical iOS device.

renchap commented 1 year ago

FYI there is a simple repro repo here, based on RN 0.72.1-rc1: https://github.com/renchap/react-native-36494-repro

It reproduces the initially reported issue, where the text is (partially) duplicated with multiline + maxLength when the initial text is > maxLength /2. This is due to onChangeText (and onChange) being called multiple times on render with the duplicated text.

I did not try without maxLength

renchap commented 1 year ago

I added a second repro case in my repo linked above, without maxLength, and can confirm that onChangeText (and onChange) is called once when rendering the <TextInput multiline>, regardless of the size of the initial value. This does not happen without multiline.

So there are 2 parts of this bug:

matthewmturner commented 1 year ago

I see a fix for this was added - which I tried locally and worked. how soon will the fix be released?

renchap commented 1 year ago

@matthewmturner It's planned for 0.71.8, no date yet.

ataravati commented 1 year ago

Has this already been released? I can still see the issue on Expo 48, React Native 0.71.8.

ataravati commented 1 year ago

Has this already been released? I can still see the issue on Expo 48, React Native 0.71.8.

Never mind! I just saw that it is listed as one of the changes in React Native 0.72:

https://github.com/facebook/react-native/blob/main/CHANGELOG.md#ios-specific-9

alexkhazzam commented 1 year ago

just encountered this problem yesterday. Took me a while to figure out what was going on. A simple but dirty solution is to use the useRef hook. Set the initial hook to 0. in the onChangeText method, check to see if the useRef value is 0; if it is, do not execute the callback, otherwise execute the callback. From then on, keep incrementing the useRef value. This will persist across state changes

tarikfp commented 1 year ago

Experiencing this issue in react-native@0.72.3. It is related to the multilineprop itself rather than maxLength. When text input has multiline set to true, onChangeText callback invokes itself on the next batch/render. When you set multiline to false, the bug disappears, meaning onChangeText triggers only when we are actually setting the value.

Created basic snack example below, expo version 48, rn version 0.71.x:

https://snack.expo.dev/@tarikfp/textinput-onchangetext-bug-multiline

zcdev02 commented 1 year ago

I confirm the issue. Indeed, when using multiline, OnChangeText starts to fire a large number of times. Disabling this setting resolves the issue. 0.72.3 react. this happens in a modal window on ios

douglasjunior commented 1 year ago

Experiencing this issue in react-native@0.72.3. It is related to the multilineprop itself rather than maxLength. When text input has multiline set to true, onChangeText callback invokes itself on the next batch/render. When you set multiline to false, the bug disappears, meaning onChangeText triggers only when we are actually setting the value.

Created basic snack example below:

https://snack.expo.dev/@tarikfp/textinput-onchangetext-bug-multiline

Snack uses Expo 48 which should use RN 0.71.7 or something like that, not 0.72.

What I have experienced here in my tests is when multiline=true applying masks to the value:

In all the cases, the onChangeText works fine when you don't change value programmatically, but if you do:

version description
<= 0.71.7 ⚠️ Changing value call onChangeText, then if you change value again the onChangeText will be called once more, creating an infinity loop. If you don't change value on every onChangeText, then will be fine.
>= 0.71.8 && <= 0.71.12 🚨 changing value call onChangeText, then if you change value again the onChangeText will not be called, but if you type some new char the onChangeText will not be fired in the first time.
>= 0.72.0 ⚠️ Same as RN <= 0.71.7

Something curious that I would like to comment is that since I started working with RN in 2016 we have some kind of problem with multiline inputs, it seems to be a chronic problem with this component.

nicolasdevienne commented 1 year ago

I confirm the issue. Indeed, when using multiline, OnChangeText starts to fire a large number of times. Disabling this setting resolves the issue. 0.72.3 react. this happens in a modal window on ios

same with version 0.72.4

sergey-shablenko commented 1 year ago

Found a workaround, just in case anyone needs it You can use

<TextInput 
  multiline
  numberOfLines={numberOfLines}
  defaultValue={value}
  onEndEditing={(e) => onChangeText(e.nativeEvent.text)}
/>

may not fit everyone, but at least does not go into infinite loop

douglasjunior commented 1 year ago

Updating the behavior mentioned here: https://github.com/facebook/react-native/issues/36494#issuecomment-1678773893

version description
<= 0.71.7 ⚠️ Changing value call onChangeText, then if you change value again the onChangeText will be called once more, creating an infinity loop. If you don't change value on every onChangeText, then will be fine.
>= 0.71.8 && <= 0.71.12 🚨 Changing value call onChangeText, then if you change value again the onChangeText will not be called, but if you type some new char the onChangeText will not be fired in the first time.
= 0.71.13 (latest) ⚠️ Same as RN <= 0.71.7
>= 0.72.0 ⚠️ Same as RN <= 0.71.7
josef256 commented 1 year ago

same issue on IOS with multiline set to true on rn 0.72.3 edit : as a workaround using defaultValue instead of value seems to fix it

<TextInput 
defaultValue={value}
/>
ukcasso commented 1 year ago

If you use a pre-release version RN, it is for temporary use. You try this example code. Fix automatically occur text, also can paste text.

import { Platform, TextInput } from 'react-native';
const [value, setValue] = useState(props.value || '');
const maxLength = 100;

<TextInput
  onChangeText={(newText) => {
    if (Platform.OS === 'ios' &&
        value &&
        maxLength &&
        value.length > (maxLength / 2) &&
        newText.length - value.length >= maxLength - value.length) {
      return;
    }

    setValue(newText);
  }}
  value={value}
  maxLength={maxLength}
  multiline
/>
matheusbaumgart commented 9 months ago

Still an issue on 0.73.2

jackylu0124 commented 2 months ago

just encountered this problem yesterday. Took me a while to figure out what was going on. A simple but dirty solution is to use the useRef hook. Set the initial hook to 0. in the onChangeText method, check to see if the useRef value is 0; if it is, do not execute the callback, otherwise execute the callback. From then on, keep incrementing the useRef value. This will persist across state changes

Could you please provide a reference of your alternative solution? In my case, not only is onChangeText being called repeatedly, but its value is also changed. For example, in my case, I have maxLength={8} and if I type "Abcde" and then programmtically set the state to empty string and navigate to another screen; and then coming back to the screen and programmatically setting the state to "Abcde" again, the text input then incorrectly renders the string "AbcAbcde". I have encountered very similar behavior to this issue: https://github.com/facebook/react-native/issues/44566

Thanks for your help in advance!