'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:
- Adding/removing Key to different components used in .map()
- 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 |