react-native icon indicating copy to clipboard operation
react-native copied to clipboard

(RN 79.5) TextInput onContentSizeChange is not triggered on iOS

Open Njaah-0 opened this issue 5 months ago • 21 comments

Description

I noticed that after updating to 0.79.5, TextInput.onContentSizeChange is no longer triggered even if the multiline is set to true and input does growth properly. It is only triggered once, when the component mounts.

Steps to reproduce

  1. Add any TextInput component with onContentSizeChange prop, eg. onContentSizeChange={() => Alert.alert('hello')}
  2. Render the app with iOS-simulator
  3. Press enter on input, nothing happens.

React Native Version

0.79.5

Affected Platforms

Runtime - iOS

Output of npx @react-native-community/cli info

info Fetching system and libraries information...
System:
  OS: macOS 14.6.1
  CPU: (8) arm64 Apple M1
  Memory: 150.19 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 20.18.1
    path: /usr/local/bin/node
  Yarn:
    version: 1.22.22
    path: ~/.npm-global/bin/yarn
  npm:
    version: 10.8.2
    path: /usr/local/bin/npm
  Watchman:
    version: 2025.04.14.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/lib/ruby/gems/3.0.0/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  Android SDK: Not Found
IDEs:
  Android Studio: 2024.1 AI-241.18034.62.2411.12071903
  Xcode:
    version: 16.2/16C5032a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 22.0.1
    path: /usr/bin/javac
  Ruby:
    version: 3.4.2
    path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.0.0
    wanted: 19.0.0
  react-native:
    installed: 0.79.5
    wanted: 0.79.5
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

info React Native v0.80.2 is now available (your project is running on v0.79.5).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.80.2
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.79.5&to=0.80.2
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".

Stacktrace or Logs

None

MANDATORY Reproducer

https://snack.expo.dev/@njaah/textinput-issue?platform=ios

Screenshots and Videos

No response

Njaah-0 avatar Jul 26 '25 20:07 Njaah-0

Thanks for the bug, I was able to reproduce this on RN Tester. Was this working on RN 0.78?

riteshshukla04 avatar Jul 27 '25 09:07 riteshshukla04

@riteshshukla04 I also started experiencing this problem after updating expo to v53. I don't know if it was working on 0.78 but it was definitely working on 0.76.9 which was the version I was using before the expo update.

nfmshow avatar Jul 28 '25 13:07 nfmshow

Thanks for the bug, I was able to reproduce this on RN Tester. Was this working on RN 0.78?

I am not sure about 0.78, my previous version was Expo 50 and I can't remember which RN version that uses. But it was definitely working back then.

Njaah-0 avatar Jul 29 '25 11:07 Njaah-0

Also, in case someone is struggling with this issue, you can quite easily overcome it with the help of the onLayout prop, which is still triggered correctly.

react-native-gifted-chat is affected by this issue, as it doesn't report composer height correctly.

Njaah-0 avatar Jul 29 '25 11:07 Njaah-0

@Njaah-0 Thanks 🙏

nfmshow avatar Jul 29 '25 13:07 nfmshow

it reproducible on latest react-native version 0.81.0

deepak9705 avatar Aug 18 '25 14:08 deepak9705

I believe this will be helpful to you: https://github.com/facebook/react-native/pull/50782

sunujun avatar Aug 19 '25 08:08 sunujun

I am also experiencing this issue with 0.81.1. @Njaah-0 can you elaborate on how onLayout fixes this? On my end it looks like that is also not firing on ios.

cmhac avatar Sep 01 '25 20:09 cmhac

@cmhac

You can render Text with the same fontSize & lineHeight & width and use onLayout to set the height of the input

uen avatar Sep 04 '25 19:09 uen

@uen an example could be better rather than just words.

abhinav-official avatar Sep 05 '25 02:09 abhinav-official

I also had issues using onLayout.

@uen's solution worked well. Hopefully I can delete this code soon, yikes. 😅

    const [text, setText] = useState<string>('');
    const [inputHeight, setInputHeight] = useState(100); // initial height

    // The Text component is used to measure the height of the text input content, as onContentSizeChange is not triggering correctly on ios.
    // https://github.com/facebook/react-native/issues/52854
    return (
        <KeyboardAwareScrollView className="p-3">
            <Text
                onLayout={(event) => {
                    setInputHeight(event.nativeEvent.layout.height);
                }}
                className="opacity-0 absolute"
            >
                {text}
            </Text>
            <TextInput
                textAlignVertical="top" // keeps the text anchored at the top
                multiline={true}
                onChangeText={setText}
                className="text-md"
                style={{ color: colors.text, height: Math.max(100, inputHeight) + 30 }}
                underlineColorAndroid="transparent"
                autoFocus={true}
                scrollEnabled={false}
            />
        </KeyboardAwareScrollView>
    );

patrickomeara avatar Oct 14 '25 03:10 patrickomeara

Hello, same bug here. The multiline TextInput does not grow automatically anymore, and onContentSizeChange is not called either. I just upgraded to the latest Expo recently. It was working on previous React-Native versions!

mhammerc avatar Nov 09 '25 11:11 mhammerc

Same issue here, using Expo 54.0.22 with react-native: 0.81.5

BouarourMohammed avatar Nov 09 '25 13:11 BouarourMohammed

In my case, to fix this issue I had to execute this once: textInputRef.current?.setNativeProps({ text });

More details : https://github.com/facebook/react-native/issues/52854#issuecomment-3514945557

BouarourMohammed avatar Nov 09 '25 22:11 BouarourMohammed

In my case, to fix this issue I had to execute this once: textInputRef.current?.setNativeProps({ text });

Lovely, I was impacted by both bugs:

  1. onContentSizeChange is never triggered except on mount
  2. TextInput with multiline doesn't grow automatically anymore on new RN version.

By doing @BouarourMohammed it fixes the bugs

PierreCapo avatar Nov 10 '25 15:11 PierreCapo

In my case, to fix this issue I had to execute this once: textInputRef.current?.setNativeProps({ text });

Lovely, I was impacted by both bugs:

  1. onContentSizeChange is never triggered except on mount
  2. TextInput with multiline doesn't grow automatically anymore on new RN version.

By doing @BouarourMohammed it fixes the bug

Did it solved both bugs? Because I'm trying to fix the TextInput multiline autogrow by running setNativeProps as @BouarourMohammed said when the TextInput onLayout event is fired and it's not working: https://snack.expo.dev/@patosala/multiline-textinput-inside-scrollview

PatoSala avatar Nov 11 '25 01:11 PatoSala

@PatoSala What I did to fix it was using a useState on iOS, far from ideal as it forces re-rendering the input but at least it auto grows...

// MultilineInput.ios.tsx
export function MultilineInput({
  value,
  onChangeText,
  ...restProps
}: TextInputProps) {
  const [tmpValue, setTmpValue] = useState(value);

  function handleTextChange(value: string) {
    if (onChangeText) {
      onChangeText(value);
    }

    setTmpValue(value);
  }

  return (
    <TextInput
      multiline
      value={tmpValue}
      onChangeText={handleTextChange}
      {...restProps}
    />
  );
}

kamui545 avatar Nov 11 '25 02:11 kamui545

@PatoSala To make this work, the text value needs at least one character when using setNativeProps. Update your code accordingly and it should work.

  const handleOnLayout = () => {
    inputRef.current.setNativeProps({
      text: "A"
    });
  }

Or you can initialize it at mount—it's up to you how you want to tweak it—just make sure it only runs once at the start: For example: https://snack.expo.dev/@bouarourmohammed/multiline-textinput-inside-scrollview

import { StyleSheet, View, TextInput } from 'react-native';
import { useRef } from "react";

export default function App() {
  const inputRef = useRef(null);
  const initialised = useRef(false)

  return (
    <View style={{
      paddingTop: 100,
      flex: 1
    }}>
        <TextInput
          ref={inputRef}
          onChangeText={(text)=> {
            if(!initialised.current && text?.trim()){
              initialised.current = true
              inputRef.current?.setNativeProps({text})
            }
          }}
          multiline={true}
          scrollEnabled={false}
          onContentSizeChange={(e) => console.log(e)}
        />
    </View>
  );
}

BouarourMohammed avatar Nov 11 '25 04:11 BouarourMohammed

@BouarourMohammed you are right, it needs to at least have one chacracter. I really need the TextInput to be empty so I tried setting text to some value and then set it again but this time empty, like this:

const handleOnLayout = () => {
    inputRef.current.setNativeProps({
      text: " "
    });
   inputRef.current.setNativeProps({
      text: " "
    });
  }

But it doesn't work. I guess we'll have to wait till someone fixes this.

PatoSala avatar Nov 11 '25 11:11 PatoSala

Considering my whole app has a single font size for my inputs, I was able to come up with this "hack".

  1. I used the onLayout to check the width of my input and I counted how many characters I was able to type within that width (39 chars). Example: my input had 392 of width, my input has 16 of padding on left and right (32 in total) and I could type 39 chars, so:
392 (input) - 32 (horizontal paddings)
/ 39 characteres
= 
each character takes ~9.23 of width
  1. Added state for input width const [inputHeight, setInputHeight] = useState(DEFAULT_HEIGHT); const [inputWidth, setInputWidth] = useState(0);

  2. Implement height calculation based on text content

  useEffect(() => {
    if (!multiline || !inputWidth || !value) {
      if (multiline && minHeight) {
        setInputHeight(minHeight);
      }
      return;
    }

    // Subtract horizontal padding (e.g., 16px * 2 = 32px)
    const paddingHorizontal = SPACING.md * 2;
    const availableWidth = inputWidth - paddingHorizontal;

    // Calibrate: test how many characters fit per line
    // Example: 360px = 39 chars → ~9.23px per character
    const charWidth = 9.23; // ADJUST THIS VALUE FOR YOUR FONT
    const charsPerLine = Math.floor(availableWidth / charWidth);

    // Count lines (including explicit \n breaks)
    const lines = value.split('\n');
    let totalLines = 0;
    lines.forEach(line => {
      if (line.length === 0) {
        totalLines += 1;
      } else {
        totalLines += Math.ceil(line.length / charsPerLine);
      }
    });

    // Calculate height (adjust lineHeight for your font)
    const lineHeight = 24; // font-size * 1.5
    const paddingVertical = SPACING.md + SPACING.sm;
    let calculatedHeight = (totalLines * lineHeight) + paddingVertical;

    // Apply constraints
    if (minHeight && calculatedHeight < minHeight) {
      calculatedHeight = minHeight;
    }
    if (maxHeight && calculatedHeight > maxHeight) {
      calculatedHeight = maxHeight;
    }

    setInputHeight(Math.ceil(calculatedHeight));
  }, [value, inputWidth, multiline, minHeight, maxHeight]);
  1. Capture input width
  <TextInput
    onLayout={(e) => {
      setInputWidth(e.nativeEvent.layout.width);
    }}
    // ...
  />
  1. Apply calculated height and disable scroll
  <TextInput
    multiline={multiline}
    style={[
      multiline ? {
        height: inputHeight,
        minHeight: minHeight || DEFAULT_HEIGHT,
        maxHeight: maxHeight,
      } : { height: DEFAULT_HEIGHT }
    ]}
  />

If the iPhone has some accessibility tool turned on, like increased font size for the OS, this calculation may need to be adjusted. Something worth considering is using

import { PixelRatio } from "react-native";
(...)
const charWidth = 9.23 * PixelRatio.getFontScale();

marcelino-borges avatar Dec 07 '25 15:12 marcelino-borges

In case you're struggling with this issue, multiline is only working on controlled inputs.

mendozammatias avatar Dec 15 '25 06:12 mendozammatias

In case you're struggling with this issue, multiline is only working on controlled inputs.

Thank you! I've been trying a bunch of different things and it ended up being this for me. Uncontrolled inputs seem to return the same contentSize values even after updating the value.

jamesedmonston avatar Dec 22 '25 11:12 jamesedmonston

Most of the suggestions above didn't work in my case. What helped for me was removing the height Attribute and also on the container around it and suddently it grew again with the text. So I changed it to only have the height attribute when text was getting too big.

const [text, setText] = useState<string>('')
const [textHeight, setTextHeight] = useState<number>(32)
const contentSizeChange = (event: TextInputContentSizeChangeEvent) => {
    setTextHeight(event.nativeEvent.contentSize.height)
}
...
<View
    style={[styles.container, textHeight >= 150 && { height: 150 }]}
>
    <TextInput
        value={text}
        style={[styles.input]}
        multiline={true}
        scrollEnabled={true}
        onContentSizeChange={contentSizeChange}
        onChangeText={(newText) => {
            setText(newText)
        }}
    />
</View>

In this case, when height is 150, the style is applied and again, onContentSizeChange will not trigger anymore, when text is getting bigger. But it will trigger when text is getting smaller, box will become smaller, height style is removed and trigger works in both directions again.

As soon as there was something controlling it's height (even setting minHeight) the onContentSizeChange didn't trigger anymore.

kaktuspalme avatar Dec 23 '25 16:12 kaktuspalme