React Navigation 5 headerRight button function called doesn't get updated states - react-native

In the following simplified example, a user updates the label state using the TextInput and then clicks the 'Save' button in the header. In the submit function, when the label state is requested it returns the original value '' rather than the updated value.
What changes need to be made to the navigation headerRight button to fix this issue?
Note: When the Save button is in the render view, everything works as expected, just not when it's in the header.
import React, {useState, useLayoutEffect} from 'react';
import { TouchableWithoutFeedback, View, Text, TextInput } from 'react-native';
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
const submit = () => {
//label doesn't return the updated state here
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}

Label should be passed as a dependency for the useLayouteffect, Which will make the hook run on changes
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation,label]);

Guruparan's answer is correct for the question, although I wanted to make the solution more usable for screens with many TextInputs.
To achieve that, I added an additional state called saving, which is set to true when Done is clicked. This triggers the useEffect hook to be called and therefore the submit.
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
const [saving, setSaving] = useState(false);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={() => setSaving(true)}>
<Text>Done</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
useEffect(() => {
// Check if saving to avoid calling submit on screen unmounting
if(saving){
submit()
}
}, [saving]);
const submit = () => {
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}

Related

Unable to find an element with a testID

I'm building a React Native app. Within my GigsByDay component, there is a TouchableOpacity element which, when pressed, directs the user to a GigDetails screen. I'm trying to test this particular functionality using Jest and React Native Testing Library.
I've written the following test, but have received the error:
Unable to find an element with testID: gigs-today-card
The test is as follows:
describe("gigs by week component", () => {
let navigation;
beforeEach(() => {
navigation = { navigate: jest.fn() };
});
test("that when gig listing is pressed on it redirects user to Gig Details page", () => {
render(<GigsByDay navigation={navigation} />);
const gigCard = screen.getByTestId("gigs-today-card");
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith("GigDetails");
});
});
The element it's testing is as follows:
<TouchableOpacity
testID="gigs-today-card"
style={styles.gigCard}
onPress={() =>
navigation.navigate('GigDetails', {
venue: item.venue,
gigName: item.gigName,
blurb: item.blurb,
isFree: item.isFree,
image: item.image,
genre: item.genre,
dateAndTime: {...item.dateAndTime},
tickets: item.tickets,
id:item.id
})
}>
<View style={styles.gigCard_items}>
<Image
style={styles.gigCard_items_img}
source={require('../assets/Icon_Gold_48x48.png')}
/>
<View>
<Text style={styles.gigCard_header}>{item.gigName}</Text>
<Text style={styles.gigCard_details}>{item.venue}</Text>
</View>
</View>
</TouchableOpacity>
I've tried fixing my test as follows, but to no success:
test("that when gig listing is pressed on it redirects user to Gig Details page", async () => {
render(<GigsByDay navigation={navigation} />);
await waitFor(() => {
expect(screen.getByTestId('gigs-today-card')).toBeTruthy()
})
const gigCard = screen.getByTestId("gigs-today-card");
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith("GigDetails");
});
});
Any suggestions on how to fix this? I also tried assigning the testID to the view within the TouchableOpacity element.
For context, here's the whole GigsByDay component:
import { FC } from 'react';
import { FlatList,TouchableOpacity,StyleSheet,View,Image,Text } from 'react-native'
import { listProps } from '../routes/homeStack';
import { GigObject } from '../routes/homeStack';
type ListScreenNavigationProp = listProps['navigation']
interface Props {
gigsFromSelectedDate: GigObject[],
navigation: ListScreenNavigationProp
}
const GigsByDay:FC<Props> = ({ gigsFromSelectedDate, navigation }):JSX.Element => (
<FlatList
testID='gigs-today'
data={gigsFromSelectedDate}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
testID="gigs-today-card"
style={styles.gigCard}
onPress={() =>
navigation.navigate('GigDetails', {
venue: item.venue,
gigName: item.gigName,
blurb: item.blurb,
isFree: item.isFree,
image: item.image,
genre: item.genre,
dateAndTime: {...item.dateAndTime},
tickets: item.tickets,
id:item.id
})
}>
<View style={styles.gigCard_items}>
<Image
style={styles.gigCard_items_img}
source={require('../assets/Icon_Gold_48x48.png')}
/>
<View>
<Text style={styles.gigCard_header}>{item.gigName}</Text>
<Text style={styles.gigCard_details}>{item.venue}</Text>
</View>
</View>
</TouchableOpacity>
)}
/>
)
Please pass the gigsFromSelectedDate prop with some mock array data so that the flat list would render its elements based on the array length. Currently, you are not passing it. Please check the code below.
test('that when gig listing is pressed on it redirects user to Gig Details page', () => {
const mockData = [{venue: 'some venue',
gigName: 'some gigName,
blurb: 'some blurb',
isFree: 'some isFree',
image: 'some image',
genre: 'some genre',
dateAndTime: {},
tickets: ['some ticket'],
id: 'some id'}]
const screen = render(<GigsByDay navigation={navigation} gigsFromSelectedDate={mockData} />);
const gigCard = screen.getByTestId('gigs-today-card');
fireEvent.press(gigCard);
expect(navigation.navigate).toHaveBeenCalledWith('GigDetails');
});
Have you installed below package?
please check your Package.json
"#testing-library/jest-native": "^5.4.1"
if not please install it
then import it where you have written test cases.
import '#testing-library/jest-native/extend-expect';
if it is already present in then try below properties of jest-native.
const gigCard = screen.queryByTestId("gigs-today-card");
const gigCard = screen.findByTestId("gigs-today-card");

Reset state value after navigation.goBack() to parent component didn't work

In my React Native 0.70 app, there are 2 components Home (parent) and ListSearch (child). Users enter server string in Home and search result is displayed in ListSearch. When users click navigation.goBack() on ListSearch to go back to Home, useFocusEffect from react navigation 6.x is used to reset the placeholder on search bar in Home. Here is the code in Home (parent) to reset the placeholder:
export default Home = ({ navigation}) => {
const searchHolder = "Enter search string here";
const [plcholder, setPlcholder] = useState(searchHolder);
const submitsearch = async () => {
...
setPlcholder(searchHolder);//reset place holder
navigation.navigate("ListSearch", {artworks:res, title:"Search Result"}). //<<==ListSearch is component of result displaying
}
//reset placeholder whenever the Home is focused.
useFocusEffect(
React.useCallback(() => {
setPlcholder(searchHolder); // reset the place holder search bar
},[navigation])
);
//view
return (
...
<View style={{flex:5}}>
<TextInput style={{fontSize:hp("3%")}} placeholder={plcholder} onChangeText={strChg}></TextInput>. //plcholder here
</View>
)
}
The code above didn't work. When users navigation.goBack() to Home component, the placeholder in search bar was the previous search string and was not updated.
Placeholder string is updated when you navigate to ListSearch, You should set value of TextInput to empty string, here is the code you can refer,
import { useState,useEffect } from 'react';
import { View, TextInput, TouchableOpacity, Text } from 'react-native';
export default Home = ({ navigation }) => {
const searchHolder = 'Enter search string here';
const [plcholder, setPlcholder] = useState(searchHolder);
const [text, setText] = useState();
const submitsearch = () => {
console.log('submitsearch called ', searchHolder);
setText("");
setPlcholder(searchHolder);
navigation.navigate('ListSearch');
};
//view
return (
<View style={{ flex: 5 }}>
<TouchableOpacity onPress={() => submitsearch()}>
<Text>Submit</Text>
</TouchableOpacity>
<TextInput
style={{ fontSize: 20, marginTop: 30 }}
placeholder={plcholder}
value={text}
onChangeText={(val) => setText(val)} />
</View>
);
};
Here is the demo, I've created.

Clearing Formik errors and form data - React Native

I'm using Formik and was wondering how I go about clearing the errors and form values when leaving a screen.
For example, a user tries to submit the form with no values and the errors are displayed:
When the user then navigates to a different screen and then comes back those errors are still present. Is there a way to clear these? Can I access Formik methods within a useEffect hook as an example?
This is my implementation so far:
export const SignIn = ({route, navigation}) => {
const formValidationSchema = Yup.object().shape({
signInEmail: Yup.string()
.required('Email address is required')
.email('Please provide a valid email address')
.label('Email'),
signInPassword: Yup.string()
.required('Password is required')
.label('Password'),
});
const initialFormValues = {
signInEmail: '',
signInPassword: '',
};
return (
<Formik
initialValues={initialFormValues}
validationSchema={formValidationSchema}
onSubmit={(values, formikActions) => {
handleFormSubmit(values);
}}>
{({handleChange, handleSubmit, errors}) => (
<>
<SignInForm
messages={errors}
navigation={navigation}
handleFormSubmit={handleSubmit}
/>
</>
)}
</Formik>
)
}
The problem here is that a screen does not get unmounted if we navigate to a different screen and the initial values of a Formik form will only be set on screen mount.
I have created a minimal example with one field and I navigate whenever the submit button is executed to a different Screen in a Stack.Navigator.
Notice that the onSubmit function is usually not fired if there are errors in your form fields. However, since I wanted to provide a quick test, I navigate by hand by calling the function directly.
If we navigate back by pressing the onBack button of the navigator, the form fields will be reseted to the default values and all errors will be reseted automatically.
We can trigger this by hand using the Formik innerRef prop and a focus listener.
For testing this, you should do the following.
Type something, and remove it. Notice the error message that is rendered below the input field.
Navigate to the screen using the submit button.
Go back.
Expected result: no error message.
Type something. Expected result: no error message.
Navigate on submit.
Go back.
Expected result: no error message, no content in field.
In principal, this will work with every navigator, e.g. changing a Tab in a Tab.Navigator and it will reset both, the errors and the field's content.
The key part is given by the following code snippet.
const ref = useRef(null)
const initialFormValues = {
signInEmail: '',
signInPassword: '',
};
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
if (ref?.current) {
ref.current.values = initialFormValues
ref.current.setErrors({})
console.log(ref.current)
}
});
return unsubscribe;
}, [navigation, initialFormValues]);
return (
<Formik
innerRef={ref}
isInitialValid={true}
initialValues={initialFormValues}
validationSchema={formValidationSchema}
onSubmit={(values) => {
console.log("whatever")
}}>
...
The full code is given as follows.
import React, { useRef} from 'react';
import { StyleSheet, Text, View,TextInput, Button } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import { Formik } from 'formik';
import * as Yup from 'yup';
export const SignIn = ({route, navigation}) => {
const formValidationSchema = Yup.object().shape({
signInEmail: Yup.string()
.required('Email address is required')
.label('Email'),
});
const ref = useRef(null)
const initialFormValues = {
signInEmail: '',
signInPassword: '',
};
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
if (ref?.current) {
ref.current.values = initialFormValues
ref.current.setErrors({})
console.log(ref.current)
}
});
return unsubscribe;
}, [navigation, initialFormValues]);
function handleFormSubmit(values) {
navigation.navigate("SomeScreen")
}
return (
<Formik
innerRef={ref}
isInitialValid={true}
initialValues={initialFormValues}
validationSchema={formValidationSchema}
onSubmit={(values) => {
console.log("whatever")
}}>
{({handleChange, handleSubmit, errors, values}) => (
<>
<View>
<TextInput
style={{height: 30}}
placeholder={"Placeholder mail"}
onChangeText={handleChange('signInEmail')}
value={values.signInEmail}
/>
{errors.signInEmail ?
<Text style={{ fontSize: 10, color: 'red' }}>{errors.signInEmail}</Text> : null
}
<Button onPress={() => {
handleSubmit()
handleFormSubmit()}} title="Submit" />
</View>
</>
)}
</Formik>
)
}
export function SomeOtherScreen(props) {
return <></>
}
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Form" component={SignIn} />
<Stack.Screen name="SomeScreen" component={SomeOtherScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
This should be possible with the useFocusEffect hook from react navigation. It gets triggered on first time and every time the screen comes in focus.
useFocusEffect(
React.useCallback(() => {
// set Formik Errors to empty object
setErrors({});
// Since your clearing errors, you should also reset the formik values
setSignInEmail('');
setSignInPassword('');
return () => {}; // gets called on unfocus. I don't think you need this
}, [])
);

React Native - How to submit a Formik from Header

I'm new to Formik and React Native. Currently, my goal is to submit a form via the Navigation header. All the examples I found are to submit the form from the screen page itself. I need help figure out how to properly use the handleSubmit and onSubmit property and setParams to pass the value to the navigation header properly.
Right now, I'm stuck by not sending the values from the form to the useCallBack hook.
import React, {useState, useEffect, useCallback} from 'react';
import {StyleSheet, View, Button, Text} from 'react-native';
import {Input} from 'react-native-elements';
import {useDispatch} from 'react-redux';
import Colors from '../../constants/Colors';
import {
HeaderButtons,
HeaderButton,
Item,
} from 'react-navigation-header-buttons';
import {Ionicons} from '#expo/vector-icons';
import {CommonActions} from '#react-navigation/native';
import * as prodActions from '../../store/actions/products';
import {Formik} from 'formik';
import * as Yup from 'yup';
const AddProduct = ({navigation}) => {
const dispatch = useDispatch();
const submitHandler = useCallback(() => {
dispatch(prodActions.addProduct(value));
}, [value]);
useEffect(() => {
navigation.dispatch(CommonActions.setParams({submitForm: submitHandler}));
}, [submitHandler]);
return (
<View style={styles.screen}>
<Formik
initialValues={{title: '', description: '', imageUrl: ''}}
validationSchema={Yup.object({
title: Yup.string().required('please input your title'),
description: Yup.string().required('please input your description'),
imageUrl: Yup.string()
//.email('Please input a valid email.')
.required('Please input an email address.'),
})}
onSubmit={submitHandler}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
touched,
errors,
}) => (
<View>
<Input
label="Title"
labelStyle={{color: Colors.accent}}
onChangeText={handleChange('title')}
onBlur={handleBlur('title')}
value={values.title}
// errorMessage={errors.title}
/>
{touched.title && errors.title ? (
<Text style={styles.error}>{errors.title}</Text>
) : null}
<Input
label="Description"
labelStyle={{color: Colors.accent}}
onChangeText={handleChange('description')}
onBlur={handleBlur('description')}
value={values.description}
/>
{touched.description && errors.description ? (
<Text style={styles.error}>{errors.description}</Text>
) : null}
<Input
label="Image URL"
labelStyle={{color: Colors.accent}}
onChangeText={handleChange('imageUrl')}
onBlur={handleBlur('imageUrl')}
value={values.imageUrl}
/>
{touched.imageUrl && errors.imageUrl ? (
<Text style={styles.error}>{errors.imageUrl}</Text>
) : null}
<Button onPress={handleSubmit} title="Submit" />
</View>
)}
</Formik>
</View>
);
};
const IoniconsHeaderButton = (props) => (
<HeaderButton
IconComponent={Ionicons}
iconSize={23}
color="white"
{...props}
/>
);
export const AddProductHeaderOptions = (navData) => {
const updateForm = navData.route.params.submitForm;
return {
headerRight: () => {
return (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item title="add" iconName="save-outline" onPress={updateForm} />
</HeaderButtons>
);
},
};
};
export default AddProduct;
const styles = StyleSheet.create({
screen: {
flex: 1,
marginHorizontal: 20,
marginTop: 20,
},
error: {
marginLeft: 8,
fontSize: 14,
color: 'red',
},
});
You're probably using "value" variable with an empty value. You must ref all your form data, inside your < Formik > , let's use street, for an example:
value={values.street}
error={touched.street && !isSubmitting && errors.street}
returnKeyType="next"
onSubmitEditing={() => refs.number.current?.focus()}
ref={refs.street}
So declare this ref variable first:
const refs = {
street: useRef<any>(null),
number: useRef<any>(null),
zipCode: useRef<any>(null),
//....
If you don't wanna go with REF, so at least declare the variable and try it, like that:
const [value, setValue] = useState();
Also, the VALUE name is not a good variable name because there are a lot of native things using it. But, considering that you must have taken this from an example, use useState with your form or object and you should be good.
const [formx, setFormx] = useState();
For those who use React-Navigation 5 and Formik 1.5.x+ Here is a good solution.
Declare a ref variable outside/inside of your function component to later Attach to your
let formRef;//or you can use const formRef = useRef(); inside function component
If you declare formRef outside your function component use React hook {useRef} to update the ref
formRef = useRef();
Render
<Formik innerRef={formRef} />
And finally in your header button call this
navigation.setOptions({
headerRight: () => (
<Button onPress={() => {
if (formRef.current) {
formRef.current.handleSubmit()
}
}}/>
)
});
Another solution, which I think could be better is to use the useFormik, which gives you access to all the form handlers and form meta:
const {
handleSubmit,
handleChange,
isValid,
} = useFormik({
initialValues: {...},
validationSchema: yupValidationSchema,
onSubmit: (formValues) => {
//save here...
}
});
Then you can simply pass the handleSubmit reference to your right header navigation using the useLayoutEffect as proposed in the official documentation:
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => {
return (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item title="add" iconName="save-outline" onPress={handleSubmit}/>
</HeaderButtons>
);
}
});
});
This approach is clear in my opinion, since you don't need to handle all the structural disadvantages of using the renderProps approach with
<Formkik>
{({ handleSubmit }) => (
jsx here...
)}
</Formik>

Using useState in useEffect

I'm using useState to set text from TextInput, and I'm using a useEffect to overwrite the behavior from the back button.
const [text, setText] = useState("");
<TextInput
onChangeText={text => setText(text)}
defaultValue={text}
/>
const printVal() {
console.log("text is " + text);
}
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => printVal()} />
)
});
});
This always results in the text logged being the initial value of useState. If I don't use useEffect it works, but I don't want to set navigation option with every change.
Can I get the current value from useState in my useEffect, or is another solution needed?
You forgot to add dependencies as 2nd parameter to useEffect. you can add an empty array if you want your function to be called only 1 time when component loads, or for this example you should add text as dependecy because useEffect depends on this value
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => printVal()} />
)
});
}, [text]); //<- empty array here