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

[New Arch] TextInput text change flickers if within Portal

Open codan84 opened this issue 11 months ago • 12 comments

Current behaviour

Under new arch any TextInput component (be it from react-native-paper or from react-native) within Portal (modal or dialog) will flicker when text is being changed.

Expected behaviour

Editing text is smooth.

How to reproduce?

Clone this repo: https://github.com/codan84/text-input-flicker

npm install
npm start

Open in Expo Go.

Preview

https://github.com/user-attachments/assets/444d7ce3-c29b-4893-98b4-1322e6b846a9

What have you tried so far?

Nothing, I see loads of issues related to Portal under new arch so I guess this is another one?

Your Environment

software version
ios 18.1.1
android 10
react-native 0.76.5
react-native-paper 5.12.5
expo sdk 52

codan84 avatar Dec 19 '24 23:12 codan84

same problem

Ademivaldo avatar Dec 20 '24 16:12 Ademivaldo

Same problem. Does anyone know a fix?

BrandonHowe avatar Dec 26 '24 00:12 BrandonHowe

There's no fix atm afaik

codan84 avatar Dec 26 '24 20:12 codan84

For the time being I decided to just use standard react-native modal and add semi-transparrent background etc (bugs like that must not stop from going to prod), might actually stick to this as it's much simpler than paper approach:

import { PropsWithChildren } from "react";
import { Modal, StyleSheet, View, ViewStyle } from 'react-native'
import Animated, { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ThemedView } from "./ThemedView";

type ModalProps = {
  visible: boolean,
  hideDialog: () => void,
  contentStyle?: ViewStyle
}
type Props = PropsWithChildren<ModalProps>

export const AnimatedModal = ({ visible, hideDialog, children, contentStyle }: Props) => {
  const insets = useSafeAreaInsets()
  const keyboard = useAnimatedKeyboard()

  const animatedStyles = useAnimatedStyle(() => ({
    transform: [{ translateY: -keyboard.height.value }]
  }))

  return (
    <Modal
      animationType='fade'
      transparent={true}
      visible={visible}
      onDismiss={hideDialog}
    >
      <View style={styles.wrapper} onTouchEnd={hideDialog}>
        <Animated.View style={[ animatedStyles, styles.inner ]}>
          <ThemedView style={[ styles.content, { paddingBottom: insets.bottom }, contentStyle ]} onTouchEnd={(e) => e.stopPropagation() }>
            {children}
          </ThemedView>
        </Animated.View>
      </View>
    </Modal>
  )
}

const styles = StyleSheet.create({
  wrapper: {
    position: 'absolute',
    bottom: 0,
    top: 0,
    left: 0,
    right: 0
  },
  inner: {
    width: '100%',
    height: '100%',
    flexDirection: 'row',
    alignItems: 'flex-end',
    backgroundColor: 'rgba(230, 230, 230, 0.7)'
  },
  content: {
    minHeight: '30%',
    width: '100%',
    padding: 10,
    paddingTop: 15,
    borderTopLeftRadius: 15,
    borderTopRightRadius: 15
  }
})

codan84 avatar Jan 02 '25 12:01 codan84

Have you tried using a ref to store text input instead?

export default function HomeScreen() {
  const [ showModal, setShowModal ] = useState(false)
  const [ showDialog, setShowDialog ] = useState(false)
  const [ text, setText ] = useState('')

  const dialogText = useRef('')
  const modalText = useRef('')

  const handleDialogTextChange = useCallback((text: string) => {
    dialogText.current = text;
  }, []);

  const handleModalTextChange = useCallback((text: string) => {
    modalText.current = text;
  }, []);

  return (
    <SafeAreaProvider>
      <PaperProvider>
        <SafeAreaView>
          <Text>This input works fine:</Text>
          <TextInput value={text} onChangeText={setText} />
          <Button mode='outlined' onPress={() => setShowModal(true)}>Show modal</Button>
          <Button mode='outlined' onPress={() => setShowDialog(true)}>Show dialog</Button>
        </SafeAreaView>
        <Portal>
          <Modal
            visible={showModal}
            onDismiss={() => setShowModal(false)}
            contentContainerStyle={styles.modalStyle}>
            <Text>This input flickers:</Text>
            <TextInput defaultValue='' onChangeText={handleModalTextChange} />
          </Modal>
          <Dialog visible={showDialog} onDismiss={() => setShowDialog(false)}>
            <Dialog.Content>
              <Text>This input flickers:</Text>
              <TextInput defaultValue='' onChangeText={handleDialogTextChange} />
            </Dialog.Content>
          </Dialog>
        </Portal>
      </PaperProvider>
    </SafeAreaProvider>
  );
}

tonihm96 avatar Jan 03 '25 10:01 tonihm96

Have you tried using a ref to store text input instead?

export default function HomeScreen() {
  const [ showModal, setShowModal ] = useState(false)
  const [ showDialog, setShowDialog ] = useState(false)
  const [ text, setText ] = useState('')

  const dialogText = useRef('')
  const modalText = useRef('')

  const handleDialogTextChange = useCallback((text: string) => {
    dialogText.current = text;
  }, []);

  const handleModalTextChange = useCallback((text: string) => {
    modalText.current = text;
  }, []);

  return (
    <SafeAreaProvider>
      <PaperProvider>
        <SafeAreaView>
          <Text>This input works fine:</Text>
          <TextInput value={text} onChangeText={setText} />
          <Button mode='outlined' onPress={() => setShowModal(true)}>Show modal</Button>
          <Button mode='outlined' onPress={() => setShowDialog(true)}>Show dialog</Button>
        </SafeAreaView>
        <Portal>
          <Modal
            visible={showModal}
            onDismiss={() => setShowModal(false)}
            contentContainerStyle={styles.modalStyle}>
            <Text>This input flickers:</Text>
            <TextInput defaultValue='' onChangeText={handleModalTextChange} />
          </Modal>
          <Dialog visible={showDialog} onDismiss={() => setShowDialog(false)}>
            <Dialog.Content>
              <Text>This input flickers:</Text>
              <TextInput defaultValue='' onChangeText={handleDialogTextChange} />
            </Dialog.Content>
          </Dialog>
        </Portal>
      </PaperProvider>
    </SafeAreaProvider>
  );
}

Thanks for suggestion. No I haven't. And I probably won't. I am reporting the bug here so that it can be tracked. I won't be using paper's Modals/Portals. I decided in favour of my alternative approach above.

codan84 avatar Jan 04 '25 00:01 codan84

I am having the same issue with the new architecture,

"react-native-paper": "^5.13.1", "react-native": "0.76.6",

Prajwaltechversant avatar Jan 30 '25 09:01 Prajwaltechversant

Same issue with me on latest react-native-paper.

MeesterMarcus avatar Feb 20 '25 04:02 MeesterMarcus

I had the same issue, for me the flickering does not happen, when not passing value to the TextInput instead only passing onChangeText as in the example from @tonihm96 but is also works without using a ref

Blubl avatar Mar 12 '25 12:03 Blubl

I remember encountering this issue about a year ago, I opted for uncontrolled TextInput back then. Just ran into this in another application. I guess it hasn't been solved yet...

maxymczech avatar Mar 30 '25 22:03 maxymczech

It seems that this won't happen after upgrading to Expo 53.

FSpark avatar May 22 '25 14:05 FSpark

Trying to "upgrade" my text input modals to match our other modal implementations with react-native-paper and then spent the day debugging why my text inputs are continuously flickering:

https://github.com/user-attachments/assets/f3ec1874-ce1d-4846-a0e3-0ae7f3208ac0

The flicker does not occur if I don't use a Portal component. Nor does it happen if I use Portal.Host.

onChangeText is being called repeatedly with the new value, then the original, then the new, ad infinitum.

My component code:

export const TextAreaModal: FC<TextAreaModalProps> = (props) => {
  const {
    isVisible,
    setModalVisible,
    onSaveText,
    value = "",
  } = props

  const [localText, setLocalText] = useState(value)

  const onChangeText = (text) => {
    console.log("onChangeText", text)
    setLocalText(text)
  }
  
  const closeModal = () => {
    onSaveText?.(localText)
    setModalVisible?.(false)
  }

  return (
    <Portal>
      <Modal
        visible={isVisible}
        onDismiss={closeModal}
        contentContainerStyle={styles.modalContentContainer}
        testID="modal-component"
        style={styles.modal}
        theme={{ isV3: false }}
      >
        <View style={styles.modalBody}>
          <TextInput
            onChangeText={onChangeText}
            value={localText}
            autoFocus
            placeholder="Write a note..."
            multiline
            style={styles.textWrapper}
            inputStyle={styles.textInput}
          />
        </View>
      </Modal>
    </Portal>
  )
}

jfries289 avatar May 30 '25 21:05 jfries289

@lukewalczak Can we get some 👀 on this? This issue keeps popping up for us. One further detail, it appears to only happen when multiline is on.

jfries289 avatar Jul 15 '25 21:07 jfries289

Try autoCorrect={false}

jerrym7 avatar Aug 31 '25 09:08 jerrym7