In my React Native project I want to add edit, go back and save button in the header of my form screen.
To manage my form, I use react-hook-form.
The header come from react-navigation and I use the navigation.setOptions function to add my buttons.
This work well for the edit or go back button but save button don't fire handleSubmit function provide by react-hook-form.
If I put the same button in another place in my page, that work well.
const MemberScreen = (navigation: any) => {
const { control, handleSubmit, formState: { errors } } = useForm();
const [editMode, setEditMode] = useState(false);
useLayoutEffect(() => {
let title = "";
let headerRight: any;
let headerLeft: any;
if (editMode) {
title = "edit form"
headerRight = () => (<TouchableOpacity onPress={() => { save() }}><MaterialCommunityIcons name="content-save" color={AppConst.primaryColor} size={32} style={styles.iconItem} /></TouchableOpacity>)
headerLeft = () => (<TouchableOpacity onPress={() => { toggleEdit() }}><MaterialCommunityIcons name="close" color={AppConst.primaryColor} size={32} style={styles.iconItem} /></TouchableOpacity>)
} else {
headerRight = () => (<TouchableOpacity onPress={() => { toggleEdit() }}><MaterialCommunityIcons name="pencil" color={AppConst.primaryColor} size={32} style={styles.iconItem} /></TouchableOpacity>)
headerLeft = () => headerLeftWithBack(navigation);
}
navigation.navigation.setOptions({ title: title, headerRight: headerRight, headerLeft: headerLeft });
}, [navigation, editMode])
const toggleEdit = () => {
setEditMode(!editMode);
}
const save = () => {
handleSubmit((data) => {
onSubmit(data)
})
}
const onSubmit = async (data: any) => {
let body = { id: member.id, ...data }
// ...
}
return // ...
}
Do you have any idea or solution to fix this problem ?
This fix my problem because i miss parentheses :
const save = () => {
handleSubmit((data) => {
onSubmit(data)
})()
}
Related
Here you can see the gif
Here is my whole Navigator functional component. I'm trying to implement two tabs using Tab Navigator. One to display the cryptos and the other to display the forex data.
The problem is, when I try to load more data on reaching the flatlist's end, the flatlist is scrolling to the top since I'm making a state change [page+1].
const Navigator = () => {
const Tab = createMaterialTopTabNavigator();
const renderItems = ({ item }) => (
<Text>{item.name}<Text>
);
const fetchMarketData = async () => {
console.log("Fetching");
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
const ids = data.map((item) => item.id);
let newData = marketData.filter((item) => !ids.includes(item.id));
setData([...data, ...newData]);
setFetching(false);
} else {
setFetching(false);
Alert.alert(marketData, "Sorry for the inconvenience");
}
};
useEffect(() => {
setFetching(true);
const data = async () => {
await fetchMarketData();
};
}, [page]);
const handleLoadMore = async () => {
setFetching(true);
setPage((page) => page + 1);
};
const ScreenA = () => (
<FlatList
data={data}
style={{ backgroundColor: "white" }}
keyExtractor={(item) => item.id}
renderItem={renderItems}
scrollEventThrottle={16}
onEndReached={handleLoadMore}
onEndReachedThreshold={0}
/>
);
return (
<Tab.Navigator
screenOptions={({ route }) => screenOptions(route)}
keyboardDismissMode="auto"
>
<Tab.Screen name="Crypto" component={ScreenA} />
<Tab.Screen name="Forex" component={ScreenC} />
</Tab.Navigator>
);
};
export default Navigator;
OnEndReached is firing the handleLoadMore function and after the state change on data, the Flatlist is scrolling to the top.
1st reason
you have typo in "fetchMarketData", how exactly u get "newData" because i cant see it anywhere, maybe it should be "marketData" if not then u adding SAME old data PLUS undefined[...data, ...undefined]
2nd reason
reason why is that u call setPage(page + 1) and then "fetchMarketData" this is bad why ? because setState is async and it can be changed instant or after 5 secound, so u dont know when its changed and this is why we have hooks, you can use "useEffect" to handle this
change your "handleLoadMore" for example like this
const handleLoadMore = () => {
setPage(page + 1);
};
add useEffect hook that runs when "page" state changes
React.useEffect(() => {
(async() => {
setFetching(true)
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
setData([...data, ...marketData]);
} else {
Alert.alert(marketData, "Sorry for the inconvenience");
}
setFetching(false)
})()
}, [page])
I have set up a store function
export const storeData = async text => {
try {
await AsyncStorage.getItem("notes")
.then((notes) => {
const noteList = notes ? JSON.parse(notes) : [];
noteList.push(text);
AsyncStorage.setItem('notes', JSON.stringify(noteList));
});
} catch (error) {
console.log("error saving" + error);
}
};
When calling from the header back button it works as intended
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
storeData(text).then(() => {
navigation.goBack();
}
}} />
)
});
But when using it from the hardware back button it gives me an "unhandled promise rejection, undefined is not an object. evaluating _this.navigation".
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
storeData(text).then(() => {
this.navigation.goBack();
});
});
return () => backHandler.remove();
}, [text]);
Can anyone see what might cause this behaviour?
replace this by props. thiskey word is used mainly in class components here i its a functional components so navigation is reached by props.navigation
The full code would look like
function EditNoteScreen({ navigation }) {
const [text, setText] = useState("");
const backAction = () => {
storeData(text).then(() => {
Keyboard.dismiss();
navigation.goBack();
});
}
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
backAction();
});
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
backAction();
}} />
)
});
return () => backHandler.remove();
}, [text]);
If I simply have my storage function run with the hardware back press the code will work and the hardware back buttons default behavior will take me back, but then the new item will not show up until refreshed, which is why i want the back behavior delayed until saving is done.
One way to ignore this would simply be to update the flatlist again on state change, but I would rather have the information there from the refresh rather then popping in.
To learn myself react-native I am building an app that contains a FlatList filled with tickets with help of redux. When I try to filter through the tickets by typing in a number, the list gets filtered but only for 1 second. After that it gives a list of all tickets again. I have trouble finding the the logical error behind my beginner code. Any help would be appreciated.
I pasted the list below:
const AllTicketList = ({ navigation, ticket: { allTickets }, getTickets }) => {
useEffect(() => {
getTickets();
}, []);
const [enteredValue, setEnteredValue] = useState();
const [selectedNumber, setSelectedNumber] = useState(false);
const [displayedTickets, setDisplayedTickets] = useState();
const [confirmed, setConfirmed] = useState(false);
useEffect(() => {
setDisplayedTickets(allTickets);
});
const confirmInputHandler = () => {
const chosenNumber = parseInt(enteredValue);
if (isNaN(chosenNumber) || chosenNumber <= 0) {
Alert.alert(
'Invalid number',
'The number of upvotes has to be greater than 0.',
[{ text: 'Ok', style: 'destructive', onPress: resetInputHandler }]
);
return;
}
setConfirmed(true);
setSelectedNumber(chosenNumber);
Keyboard.dismiss();
};
const resetInputHandler = () => {
setEnteredValue('');
setConfirmed(false);
};
const numberInputHandler = inputText => {
setEnteredValue(inputText.replace(/[^0-9]/g, ''));
};
if (confirmed) {
const foundTickets = displayedTickets.filter(t => t.numberOfVotes >= selectedNumber);
setDisplayedTickets(foundTickets);
setConfirmed(false);
}
return (
<View>
<SearchBarUpvotes
numberInputHandler={numberInputHandler}
confirmInputHandler={confirmInputHandler}
enteredValue={enteredValue}
/>
<FlatList
removeClippedSubviews={false}
data={displayedTickets}
renderItem={({ item }) => (
<TicketItem ticket={item} navigation={navigation} />
)}
keyExtractor={item => item.id}
/>
</View>
);
};
const mapStateToProps = state => ({
ticket: state.ticket
});
export default connect(mapStateToProps, {
getTickets
})(AllTicketList);
The problem is in your second useEffect hook:
useEffect(() => {
setDisplayedTickets(allTickets);
});
This effect, will set the displayedTickets to allTickets on every re-render.
So here's what happens:
1. When you filter the tickets, you're changing the state, and you're setting the displatedTickets to be the filtered tickets: setDisplayedTickets(foundTickets);.
2. The displayedTickets is updated, the component is re-rendered, you see the new tickets for a second, and as soon as it is re-rendered, that effect is executing again and it sets the displayedTickets to allTickets again: setDisplayedTickets(allTickets);.
So here's my advice:
1. Remove the second useEffect - that will prevent the displayedTickets to be set again to allTickets on every re-render .
2. In your flatlist change the data to displayedTickets || allTickets. In this way, when the tickets will be unfiltered - the list will display the allTickets and as soon as you filter them, the list will display the displayedTickets.
So here's how your final code should look like:
const AllTicketList = ({ navigation, ticket: { allTickets }, getTickets }) => {
useEffect(() => {
getTickets();
}, []);
const [enteredValue, setEnteredValue] = useState();
const [selectedNumber, setSelectedNumber] = useState(false);
const [displayedTickets, setDisplayedTickets] = useState();
const [confirmed, setConfirmed] = useState(false);
// Remove this effect
//useEffect(() => {
// setDisplayedTickets(allTickets);
//});
const confirmInputHandler = () => {
const chosenNumber = parseInt(enteredValue);
if (isNaN(chosenNumber) || chosenNumber <= 0) {
Alert.alert(
'Invalid number',
'The number of upvotes has to be greater than 0.',
[{ text: 'Ok', style: 'destructive', onPress: resetInputHandler }]
);
return;
}
setConfirmed(true);
setSelectedNumber(chosenNumber);
Keyboard.dismiss();
};
const resetInputHandler = () => {
setEnteredValue('');
setConfirmed(false);
};
const numberInputHandler = inputText => {
setEnteredValue(inputText.replace(/[^0-9]/g, ''));
};
if (confirmed) {
const foundTickets = displayedTickets.filter(t => t.numberOfVotes >= selectedNumber);
setDisplayedTickets(foundTickets);
setConfirmed(false);
}
return (
<View>
<SearchBarUpvotes
numberInputHandler={numberInputHandler}
confirmInputHandler={confirmInputHandler}
enteredValue={enteredValue}
/>
<FlatList
removeClippedSubviews={false}
data={displayedTickets || allTickets} /* <-- displayedTickets || allTickets instead of displayedTickets */
renderItem={({ item }) => (
<TicketItem ticket={item} navigation={navigation} />
)}
keyExtractor={item => item.id}
/>
</View>
);
};
const mapStateToProps = state => ({
ticket: state.ticket
});
export default connect(mapStateToProps, {
getTickets
})(AllTicketList);
I'm new to react-native and formik and I encountered this problem that I'm trying to build up.
How can I fire a function in headerRight using Formik? I have updateCorporation function that will do fire api, and formik will do the job to fire this function and after I press the Update button, but the results are undefined
I didn`t understand why its happening.
File_1.js
const CorporationContainer = (props) => {
const {
navigation,
} = props;
const updateCorporation = (values) => {
// do patch stuff with values
// but its undefined
};
useEffect(() => {
navigation.setParams({ updateCorporation: updateCorporation.bind() });
}, []);
return (
<Corporation
updateCorporation={updateCorporation} />
);
};
CorporationContainer.navigationOptions = ({ navigation }) => ({
headerRight: (
<EditBtn
onPress={() => navigation.state.params.updateCorporation()}
>
<EditText>Update</EditText>
</EditBtn>
),
});
export default CorporationContainer;
File_2.js
const Corporation = (props) => {
const {
updateCorporation,
} = props;
const emailField = useRef(null);
const validationSchema = yup.object().shape({
email: yup.string()
.ensure()
.email('Email must be valid')
.required('Email'),
});
return (
<Formik
initialValues={{
email,
}}
onSubmit={values => updateCorporation(values)}
validateOnBlur={false}
validateOnChange={false}
validationSchema={validationSchema}
>
{(formProps) => {
const {
errors,
setFieldValue,
values,
} = formProps;
return (
<Container>
<Input
name="email"
placeholder="Email Corporation"
textContentType="emailAddress"
keyboardType="email-address"
returnKeyType="done"
autoCapitalize="none"
autoCorrect={false}
ref={emailField}
value={values.email}
onChangeText={setFieldValue}
editable={!email}
error={errors.email}}
/>
</Container>
);
}}
</Formik>
);
};
export default Corporation;
In File_1.js I had to use withForm and remove all Formik things in File_2.js and use the props instead.
const CorporationContainer = (props) => {
const {
navigation,
handleSubmit,
errors,
setFieldValue,
values,
} = props;
useEffect(() => {
navigation.setParams({ updateCorporation: handleSubmit.bind() });
}, []);
return (
<ProfileProfessional
errors={errors}
setFieldValue={setFieldValue}
values={values}
/>
);
};
CorporationContainer.navigationOptions = ({ navigation }) => ({
headerRight: (
<EditBtn
onPress={() => navigation.state.params.updateCorporation()}
>
<EditText>Editar</EditText>
</EditBtn>
),
});
export default withFormik({
// ...
})(CorporationContainer);
Formik author here...
Haven't tried this out, and idk exactly how navigation binding works, but you want to bind Formik's submitForm() prop to the navigation and not the updateCorporation function. However, You will need to do this where you have access to Formik props/context (i.e. as a child of <Formik>).
import React from 'react'
import { connect } from 'formik'
const updateCorporation = (values) => {
// do patch stuff with values
// but its undefined
};
const BindSubmit = connect(({ formik, navigation }) => {
useEffect(() => {
navigation.setParams({ updateCorporation: submitForm.bind() });
}, []);
return null
})
// ... and then just render it somewhere under Formik
const Corporation = () => {
return (
<Formik>
<BindSubmit />
{/* ... same */}
</Formik>
)
}
how can i call a static function (static navigationOptions) from any other function in react native?
It fails when using the this keyword, but is it possible to render static navigationOptions again by calling it?
If you want to change navigation options dynamically, try like this
static navigationOptions = ({ navigation }) => {
return {
title: navigation.getParam('otherParam', 'A Nested Details Screen'),
};
};
this.props.navigation.setParams({otherParam: 'Updated!'})
Another method
static navigationOptions = ({ navigation }) => {
const {state} = navigation;
return {
title: `${state.params.title}`,
};
};
ChangeThisTitle = (titleText) => {
const {setParams} = this.props.navigation;
setParams({ title: titleText })
}
call this.ChangeThisTitle('your title') wherever you want
They only way to achieve it is using navigation params. So set your headerleft property flag or value using setParams. That will solve the issue. Below mentioned code should be used in your class.
static navigationOptions = ({ navigation }) => {
const { params = {} } = navigation.state;
let buttonView =
<TouchableOpacity style={navItemStyle} activeOpacity={0.7} onPress={() => { params.logoutClick() }}>
<Text style={navItemTxt}> Logout</Text>
</TouchableOpacity >
return {
title: 'Home',
headerLeft: params.showHeaderLeft && buttonView
};
};
componentDidMount() {
this.props.navigation.setParams({
showHeaderLeft: this.props.headerFlag,
//headerFlag used above is your value and showHeaderLeft is the name of the param
});
}