Search code examples
reactjsreact-nativeuse-state

How to wait for onBlur state update when submitting form


I have a big form in a ScrollView with keyBoardShouldPersistTaps set to "handled", and are saving the the values entered into Global State when onBlur is called in a TextInput (the onChangeText is saved to local state within each component to be more performant).

The problem comes when my users are submitting the form, if they click on the submit button while still having a textinput in focus (with values in it) the submit call is run without the values from the textinput that was in focus when the submit button was pressed.

All other values in all other textinputs are submitted correctly, as the state has had time to update correctly.

How can I make sure that the onBlur and useState (which is async I know) is updated before the submit function is called?

Simplified code to explain my challenge (expo snack):

import * as React from 'react';
import { ScrollView, StyleSheet, Button, Alert, TextInput, Keyboard } from 'react-native';
import Constants from 'expo-constants';

export default function App() {
      const [globalValue, setGlobalValue] = React.useState('Useless Placeholder');
    const [localValue, setLocalValue] = React.useState(globalValue);

  return (
    <ScrollView keyboardShouldPersistTaps={"handled"} style={styles.container}>
    <TextInput
      style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
      onChangeText={text => setLocalValue(text)}
      value={localValue}
      onBlur={() => setGlobalValue(localValue)}
    />
            <Button
        title="Press me"
        onPress={() => {Keyboard.dismiss(); Alert.alert(globalValue);}}
      />
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
});

Steps to reproduce my problem:

  1. Enter something into the textinput
  2. Directly click the submit button
  3. You will not see what you entered in the textinput because the submission fires before the state has updated from the onBlur

Steps to reproduce a working "submission":

  1. Enter something into the textinput
  2. click anywhere else to dismiss the keyboard
  3. Click on the button and you will see what you entered into the textinput

Solution

  • OLD CODE: Can't you do as below or you have to use another state variable called 'text'?

    export default function App() {
      const placeHolder = "Useless Placeholder";
      const [value, onChangeText] = React.useState(placeHolder);
    
      return (
        <ScrollView keyboardShouldPersistTaps={"handled"} style={styles.container}>
        <TextInput
          style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
          onChangeText={text => onChangeText(text)}
          value={value}
          onBlur={() => {
            onChangeText(value || placeHolder);
          }}
        />
        <Button
            title="Press me"
            onPress={() => {Keyboard.dismiss(); Alert.alert(value);}}
        />
        </ScrollView>
      );
    }
    

    Updated code: This is the only way I can see as of now

    export default function App() {
      const placeHolder = 'Useless Placeholder';
      const [globalValue, setGlobalValue] = React.useState(placeHolder); 
      const [localValue, setLocalValue] = React.useState(globalValue);
        
      useEffect(() => {
        Keyboard.dismiss();
        if(placeHolder !== globalValue) {
          Alert.alert(globalValue);
        }      
      }, [globalValue]);
    
      return (
        <ScrollView keyboardShouldPersistTaps={"handled"} style={styles.container}>
          <TextInput
            style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
            onChangeText={text => setLocalValue(text)}
            value={localValue}
          />
          <Button
            title="Press me"
            onPress={() => {setGlobalValue(localValue)}}
          />
        </ScrollView>
      );
    }