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):
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
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