'Issue: React-Native - Keyboard closes on each keystroke for TextInput

Full disclaimer upfront for this one - I've been working with react native for around a week or two, and I suspect that I've encountered this issue without fully understanding why!

Issue: On each keystroke within a TextInput field, the keyboard closes automatically and only records the first keystroke.

Situation: I am using a prefefined array as the default for useState. The TextInput fields are called using .map() based on the current state. The onChangeText() updates the sate to capture changes to the array. The state is updated with each keystroke.

Things Tried:

  1. Adding/removing Key to different components used in .map()
  2. Adding keyboardShouldPersistTaps='handled' to the ScrollView that the .map() is called in, including all other variations avaialble

Does anyone know what is causing the keyboard to close on each keystroke, and how I can prevent this from happening whilst continuing to capture changes to the TextInput fields in the main state?

Snippet below of the code I'm working on (I've removed some of the unrelated detail):

import React, { useState } from 'react';
import {
  View,
  Text,
  Button,
  TextInput,
  SectionList,
  SafeAreaView,
  TouchableOpacity,
  ScrollView,
  Modal,
} from 'react-native';
import { Picker} from '@react-native-community/picker';



//import custom components

import { styles, Break } from './MasterStyles';
import { inputData, ingredients } from './inputData';



function addNewLoaf() {

  const [ingredientsList, setIngredientsList] = useState(ingredients);
  const [selectedLoaf, setSelectedLoaf] = useState('Regular Loaf');
  const [flourModalVisibility, setFlourModalVisibility] = useState(false);
  const [newLoaf, setNewLoaf] = useState('');

  function IngredientsRecorder() {

    return (
      <View style={styles.ingredientsContainer}>
        <View style={{flexDirection: 'column'}}>
          <View>
            <Text style={styles.metricTitle}>
              Volume of Ingredients:
            </Text>
          </View>
          {
            ingredientsList.map(e => {
              if(e.isVisible && e.ingredient){
                return (
                  <View style={{flexDirection: 'row', alignItems: 'center'}} key={e.id}>
                    <View style={{flex:2}}>
                      <Text style={styles.metricText}>{e.name}:</Text>
                    </View>
                    <View style={{flex:3}}>
                      <TextInput
                        placeholder='amount'
                        style={styles.inputText}
                        keyboardType='number-pad'
                        value={e.amount}
                        onChangeText={value => ingredientsAmountHandler(value, e.id)}
                      />
                    </View>
                    <View style={{flex:1}}>
                      <Text style={styles.ingredientsText}>{e.units}</Text>
                    </View>
                  </View>
                )
              }
            })
          }
        </View>
      </View>
    )
  }



  const ingredientsAmountHandler = (text, id) => {
    // setAmount(enteredText);

    let newArray = [...ingredientsList]
    let index = newArray.findIndex(element => element.id === id)

    newArray[index].amount = text
    setIngredientsList(newArray)
  }


  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.page}>
        <Text style={styles.titleText}>Add a New Loaf</Text>
        <Break />
        <View style={{flexDirection: 'row'}}>
          <TextInput 
            placeholder='What would you like to call your loaf?' 
            style={styles.inputText}
            onChangeText={loafNameInputHandler}
            value={newLoaf}
          />
          <Button title='Create Loaf' color='#342e29' onPress={addNewLoafHandler} />
        </View>
        <Break />
        <ScrollView styles={styles.page} keyboardShouldPersistTaps='handled'>
          <LoafSelector />
          <FlourSelector />
          <IngredientsRecorder />
        </ScrollView>
      </View>
      <Break />
    </SafeAreaView>
  );
}

  export { addNewLoaf }


Solution 1:[1]

Since you are changing the list all of your inputs are getting re-rendered. One way to avoid this would be storing your current editing text into another state value and merge it to the list after the input is submitted or lost focus. Here is the minimal example:

let defaultTemp={editingIndex:-1,text:''}

let [temp,setTemp] = useState(defaultTemp); //We will store current being edited input's data and index

{
        ingredientsList.map((e,i) => {
          if(e.isVisible && e.ingredient){
            return (
              <View style={{flexDirection: 'row', alignItems: 'center'}} key={e.id}>
                <View style={{flex:2}}>
                  <Text style={styles.metricText}>{e.name}:</Text>
                </View>
                <View style={{flex:3}}>
                  <TextInput
                    placeholder='amount'
                    style={styles.inputText}
                    keyboardType='number-pad'
                    value={temp.editingIndex===i?temp.text:e.amount}
                    //the input got focus
                    onFocus={()=>setTemp({editingIndex:i,text:e.amount})}
                    //the input lost focus
                    onBlur={()=>{
                         ingredientsAmountHandler(temp.text, e.id)
                         setTemp(defaultTemp)
                    }
                    onChangeText={text => setTemp({text,editingIndex:i})}
                  />
                </View>
                <View style={{flex:1}}>
                  <Text style={styles.ingredientsText}>{e.units}</Text>
                </View>
              </View>
            )
          }
        })
      }

Solution 2:[2]

One solution to this is to make use of the onEndEditing prop on Input. You can use local state with the onChange prop and then once the user clicks away / clicks done, the onEndEditing function is called and you can apply the local state to the parent state.


 const TextInput = ({ value, onChange }) => {
      const [currentValue, setCurrentValue] = useState(`${value}`);
      return (
          <Input
            value={currentValue}
            onChangeText={v => setCurrentValue(v)}
            onEndEditing={() => onChange(currentValue)}
          />
      );
 };

This way the parent onChange prop is only called once the field is finished being updated, rather than on each keystroke. Unless you are doing something fancy this works fine.

Solution 3:[3]

In my case with FieldArray redux-form.

I added a timeout for TextInput.

const [displayText, setDisplayText] = useState(input.value && input.value.toString());

const timerRef = useRef(null);

const handleChange = text => {
    setDisplayText(text);

    if (isDelayOnChangeNeeded) {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
      timerRef.current = setTimeout(() => {
        input && input.onChange(text);
      }, 1500);
    } else {
      input && input.onChange(text);
    }
  };

Solution 4:[4]

Replace onChangeText to onEndEditing

onEndEditing={(bidAmts) => setBidAmt(bidAmts)}

Instead of

onChangeText={(bidAmts) => setBidAmt(bidAmts)}

Solution 5:[5]

const [userName, setUserName] = useState(null);
....
function UserNameTextView() {
return (
     <View>
        <TextInput
          style={styles.textFieldContainer}
          placeholder="username"
          onChangeText={text => setUserName(text)}
          // removed below value prop
          value={userName}
        />
      </View>
);
}

For example in the above code the keypad gets dismissed on each and every keystroke. reason behind that is by default whatever we type in TextInput will be retained in the field, no need to assign it to the value property (like I did in the above code) and this causes the view to re-render and dismisses the keypad. remove the value prop and it will work as expected.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 OriHero
Solution 2 David Jones
Solution 3 Ken
Solution 4 Walia8416
Solution 5 iVignesh