react-nativeinputfrontendformik

Passing isValid from a Child component to a Parent is delayed by 1 input? (React Native, Formik)


Trying to dynamically style a submit button in the parent component (EditProfileScreen) using the data from Formik TextInput in the child component (EditInput) but the isValid boolean is always delayed making the correct return the one after the current input.

const EditInputs = ({userInfo, onInputChange}) => {
  const SignupFormSchema = Yup.object().shape({
    name: Yup.string()
      // .min(1, 'Name must be at least 1 characters')
      .max(20, 'Name has reached character limit'),
    username: Yup.string()
      .min(5, 'Username must be at least 5 characters')
      .max(20, 'Username has reached character limit')
      .matches(
        /^[A-Za-z0-9_]+$/,
        'Username can only contain letters, numbers, and underscores',
      ) // New regex
      .required('A username is required'),
    bio: Yup.string().max(200, 'Username has reached character limit'),
    email: Yup.string().email().required('An email is required'),
    number: Yup.string()
      //   .required('required')
      .matches(phoneRegExp, 'Phone number is not valid')
      .min(10, 'too short')
      .max(10, 'too long'),
    //   .nullable(),
  });

return (
    <View style={styles.wrapper}>
      <Formik
        initialValues={userInfo} // Use spread syntax for cleaner initialization
        validationSchema={SignupFormSchema}
        // validateOnBlur={true}
        validateOnChange={true}
        validateOnMount={true}
        onSubmit={values => {
          onInputChange(values, isValid); // Pass the whole 'values' object along with isValid
        }}>
        {({handleChange, handleBlur, handleSubmit, values, isValid}) => (
          <>
            <View>
              <View style={styles.inputContainer}>
                <Text
                  style={{
                    color: '#CCCCCC',
                    marginHorizontal: 18,
                    // marginVertical: 5,
                  }}>
                  Name
                </Text>
                <View
                  style={[
                    styles.inputField,
                    // {
                    //   borderColor:
                    //     1 < values.name.length ? '#52636F' : '#fa4437',
                    // },
                  ]}>
                  <TextInput
                    style={styles.input}
                    onChangeText={text => {
                      console.log(isValid);
                      handleChange('name')(text); // Update Formik state
                      onInputChange('name', text, isValid); // Pass isValid here
                    }}
                    onBlur={handleBlur('name')}
                    value={values.name}
                    placeholder="Name"
                    placeholderTextColor="#ACAFB0"
                    autoCapitalize="none"
                    autoCorrect={false}
                    textContentType="name"
                  />
                </View>

                {/* {isUsernameValid ? (
                <Image
                  style={[styles.icons, {tintColor: '#D39D34'}]}
                  source={require('../../assets/icons/excla-circle.png')}
                />
              ) : (
                <Image
                  style={[styles.icons, {tintColor: '#D39D34'}]}
                  source={require('../../assets/icons/excla-circle.png')}
                />
              )} */}
              </View>

              <View style={styles.inputContainer}>
                <View style={{flexDirection: 'row'}}>
                  <Text
                    style={{
                      color: '#CCCCCC',
                      marginLeft: 18,
                      // marginVertical: 5,
                    }}>
                    Username
                  </Text>
                  <Text
                    style={{
                      color: '#B93A21',
                      marginHorizontal: 3,
                      // marginVertical: 5,
                    }}>
                    *
                  </Text>
                </View>
                <View
                  style={[
                    styles.inputField,
                    {
                      borderColor:
                        // values.username === ''
                        //   ? '#52636F' // Default gray border if empty
                        //   :
                        values.username.length < 5
                          ? '#fa4437' // Red border for length violations
                          : !/^[A-Za-z0-9_]+$/.test(values.username)
                            ? '#fa4437' // Red border for invalid characters
                            : '#52636F', // Default gray border if valid
                    },
                  ]}>
                  <TextInput
                    style={styles.input}
                    onChangeText={text => {
                      // console.log(isValid);
                      handleChange('username')(text);
                      onInputChange('username', text, isValid); // Pass isValid here
                    }}
                    onBlur={handleBlur('username')}
                    value={values.username.trim()}
                    placeholder="Username"
                    placeholderTextColor="#ACAFB0"
                    autoCapitalize="none"
                    autoCorrect={false}
                    textContentType="username"
                  />
                </View>
                <Text
                  style={{
                    color: '#55646F',
                    marginHorizontal: 18,
                    fontSize: 12,
                    // marginVertical: 5,
                  }}>
                  You can only change your username once every 7 days.
                </Text>
              </View>

              <View style={styles.inputContainer}>
                <Text
                  style={{
                    color: '#CCCCCC',
                    marginHorizontal: 18,
                    // marginVertical: 5,
                  }}>
                  Bio
                </Text>
                <View
                  style={[
                    styles.inputField,
                    {
                      borderColor:
                        1 > values.bio.length || values.bio.length <= 200
                          ? '#52636F'
                          : '#fa4437',
                      // height: 100,
                    },
                  ]}>
                  <TextInput
                    style={[styles.input, {textAlignVertical: 'top'}]}
                    placeholderTextColor="#ACAFB0"
                    placeholder="Bio"
                    autoCapitalize="none"
                    autoCorrect={true}
                    textContentType="none"
                    onChangeText={text => {
                      handleChange('bio')(text);
                      onInputChange('bio', text, isValid); // Pass isValid here
                    }}
                    onBlur={handleBlur('bio')}
                    value={values.bio}
                    multiline={true}
                    numberOfLines={5}
                    maxLength={200}
                  />
                </View>
                <Text
                  style={{
                    color: '#55646F',
                    marginHorizontal: 18,
                    fontSize: 12,
                    // marginVertical: 5,
                  }}>
                  Brief description of your profile. URLs are hyperlinked.
                </Text>
              </View>

              <View style={styles.inputContainer}>
                <View style={{flexDirection: 'row'}}>
                  <Text
                    style={{
                      color: '#CCCCCC',
                      marginLeft: 18,
                      // marginVertical: 5,
                    }}>
                    Email Address
                  </Text>
                  <Text
                    style={{
                      color: '#B93A21',
                      marginHorizontal: 3,
                      // marginVertical: 5,
                    }}>
                    *
                  </Text>
                </View>
                <View
                  style={[
                    styles.inputField,
                    {
                      borderColor:
                        values.email.length < 1 || validate(values.email)
                          ? '#52636F'
                          : '#fa4437',
                    },
                  ]}>
                  <TextInput
                    style={styles.input}
                    placeholderTextColor="#ACAFB0"
                    placeholder="Email"
                    autoCapitalize="none"
                    keyboardType="email-address"
                    textContentType="emailAddress"
                    autoFocus={false}
                    onChangeText={text => {
                      handleChange('email')(text);
                      onInputChange('email', text, isValid); // Pass isValid here
                    }}
                    onBlur={handleBlur('email')}
                    value={values.email}
                  />
                  <TouchableOpacity
                    style={{
                      alignSelf: 'center',
                      // justifyContent: 'center',
                      backgroundColor: '#5034FF',
                      paddingHorizontal: 15,
                      paddingVertical: 7,
                      borderRadius: 20,
                    }}>
                    <Text style={{color: 'white'}}>Verify</Text>
                  </TouchableOpacity>
                </View>
              </View>

              <View style={styles.inputContainer}>
                <Text
                  style={{
                    color: '#CCCCCC',
                    marginHorizontal: 18,
                    // marginVertical: 5,
                  }}>
                  Phone Number
                </Text>
                <View
                  style={[
                    styles.inputField,
                    {
                      borderColor:
                        values.number === ''
                          ? '#52636F' // Default gray border if empty
                          : values.number.length > 11 ||
                              !phoneRegExp.test(values.number)
                            ? '#fa4437' // Red border for invalid input
                            : '#52636F', // Default grey border for valid input
                    },
                  ]}>
                  <TextInput
                    style={styles.input}
                    placeholderTextColor="#ACAFB0"
                    placeholder="Phone Number"
                    autoCapitalize="none"
                    autoCorrect={false}
                    keyboardType="phone-pad"
                    textContentType="telephoneNumber"
                    onChangeText={text => {
                      handleChange('number')(text);
                      onInputChange('number', text, isValid); // Pass isValid here
                    }}
                    onBlur={handleBlur('number')}
                    value={values.number}
                  />
                  <TouchableOpacity
                    style={{
                      alignSelf: 'center',
                      backgroundColor: '#5034FF',
                      paddingHorizontal: 15,
                      paddingVertical: 7,
                      borderRadius: 20,
                    }}>
                    <Text style={{color: 'white'}}>Verify</Text>
                  </TouchableOpacity>
                </View>
                <ErrorMessage
                  name="number"
                  component={Text}
                  style={styles.errorText}
                />
              </View>
            </View>
          </>
        )}
      </Formik>
    </View>
  );
};
const EditProfileScreen = ({route, navigation}) => {
  const [formIsValid, setFormIsValid] = useState(true); // Assume valid initially
...
const handleInputChange = (name, value, isValid) => {
    setInputValues(prevValues => ({...prevValues, [name]: value}));
    // console.log(isValid);
    setFormIsValid(isValid); // Update form validity from EditInputs
    // setHasChanges(true);
  };

const SaveButton = () => (
    <View style={{position: 'absolute', bottom: 20, right: 30}}>
      {hasChanges && (
        <Pressable
          onPress={handleSavePress}
          style={({pressed}) => [
            styles.saveButton,
            {
              backgroundColor:
                !formIsValid || isSubmitting // Check form validity AND submission state
                  ? '#838383' // Greyed out if invalid or submitting
                  : pressed
                    ? '#772414'
                    : '#B93A21',
              opacity: pressed ? 0.5 : 1,
            },
          ]}
          disabled={!formIsValid || isSubmitting} // Disable if invalid or submitting
        >
          {isSubmitting ? (
            <ActivityIndicator color="white" />
          ) : (
            <Image
              style={styles.icon}
              source={require('../assets/icons/floppy-disk.png')}
            />
          )}
        </Pressable>
      )}
    </View>
  );

return (
    <SafeAreaView style={styles.container}>
      <>
        <ScrollView>
          <EditImages userInfo={userInfo} onImageChange={handleImageChange} />
          <EditInputs
            userInfo={inputValues}
            onInputChange={handleInputChange}
          />
        </ScrollView>
        <EditHeader navigation={navigation} userInfo={userInfo} />
        <SaveButton />
      </>
    </SafeAreaView>
  );
};

For Example-

Username Input (Only numbers, letters and underscores. Has to have 5 - 20 characters):

  1. ch_ezey (starts with username)
  2. ch_ezey1 (+ '1')
  3. ch_ezey1@ (+ '@')
  4. ch_ezey1 (- '@')
  5. ch_ezey1@ (+ '@')
  6. ch_ezey (- '1@')

Expected:

 LOG  true
 LOG  true
 LOG  false
 LOG  true
 LOG  false
 LOG  true

Result:

 LOG  true
 LOG  true
 LOG  true
 LOG  false
 LOG  true
 LOG  false

Solution

  • Passing Formik validation from a ChildComponent to a ParentComponent can be done by using a setState in a JavaScript expression. I was over complicating it. Added this within my Formik component:

    <Formik> ... (all the different components) {onValidationChange(isValid)} </Formik>

    const [isFormValid, setIsFormValid] = useState(false);

    <EditInputs ... onValidationChange={setIsFormValid} />

    With this there is no delay. However if anyone were to explain to me why the other method had a delay I would appreciate it.

    Edit:

    While the solves my initial issue, it raises a warning of:

    Cannot update a component (`EditProfileScreen`) while rendering a different component (`Formik`). To locate the bad setState() call inside `Formik`, follow the stack trace as described in https://react.dev/link/setstate-in-render
    

    All attempts to solve this warning results in the initial issue arising. Any suggestions?

    Edit 2:

    I didn't know you were able to put useEffect inside of the Formik component:

    <View style={styles.wrapper}>
      <Formik
        ...>
        {({
          handleChange,
          handleBlur,
          handleSubmit,
          values,
          isValid,
          validateForm,
        }) => (
          <>
            {
              useEffect(() => {
                onValidationChange(isValid);
              }, [isValid]) // Only call when isValid changes
            } ... </Formik>
    

    So far it works with no issues or warnings