I am trying to display something fetched via graphql in my react-native was-amplify mobile app. I can't figure out how to pass that fetched data to my render method. Here's the source code. I need to be able to show the contents of the singletour object inside render. React throws an error when I try to reference this.props.singletour inside the render method. Another thing I can't figure out is how to pass the parameter received by navigation inside render to the GetTournament graphql query. Ideally I want the id: inside GetTournament to contain navigation.getParam('itemId', 'NO-ID') and not the hardcoded id. Again, react throws an error when I access this parameter inside the async method call...ANY help would be greatly appreciated!!
class DetailsScreen extends React.Component {
async componentDidMount() {
try {
const graphqldata = await API.graphql(graphqlOperation(GetTournament, { id: "4e00bfe4-6348-47e7-9231-a8b2e722c990" }))
console.log('graphqldata:', graphqldata)
this.setState({ singletour: graphqldata.data.getTournament })
console.log('singletour:', this.state.singletour)
} catch (err) {
console.log('error: ', err)
}
}
render() {
/* 2. Get the param, provide a fallback value if not available */
const { navigation } = this.props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
<Button
title="Go to Home"
onPress={() => this.props.navigation.navigate('Home')}
/>
<Button
title="Go back"
onPress={() => this.props.navigation.goBack()}
/>
</View>
);
}
}
I think I know what you are trying to do, and it can be achieved with a refactor of your code.
This is what I would do:
Capture your navigation parameters in the constructor of your component and save them to state.
Set up an initial value for singleTour in state. Set a value for loaded in state. The loaded value will allow us to determine whether the data has come back successfully or not.
Refactor your componentDidMount so that it uses the itemId that is now stored in the state.
Refactor your console.log that checks whether you have set the state, as that is not being performed correctly.
In the render pull the values from state and handle wether the data has been loaded or not. You may wish to show some loading screen or not want to handle it at all.
Here is the refactor:
class DetailsScreen extends React.Component {
constructor (props) {
super(props);
// capture the values that you have passed via your navigation in the constructor
const { navigation } = props;
const itemId = navigation.getParam('itemId', 'NO-ID');
const otherParam = navigation.getParam('otherParam', 'some default value');
this.state = {
itemId: itemId,
otherParam: otherParam,
loaded: false,
singletour: [] // you don't state what singletour is but you should set a default value here
};
}
async componentDidMount () {
try {
// we can now use the state value for itemId as we captured it in the constructor of the component
const graphqldata = await API.graphql(graphqlOperation(GetTournament, { id: this.state.itemId }));
console.log('graphqldata:', graphqldata);
// this is a bad way to access state after it has been set,
// state is asynchronous and takes time to set.
// You would need to access set by using the callback method
// this.setState({ singletour: graphqldata.data.getTournament });
// console.log('singletour:', this.state.singletour); // <- you should never do this after you setState
// this is how you should access state after you have set it
// this will guarantee that the state has been set before the
// console.log is called, so it should show the correct value of state
this.setState({
singletour: graphqldata.data.getTournament,
loaded: true // we are going to use the loaded value to handle our render
}, () => console.log('singletour:', this.state.singletour));
} catch (err) {
console.log('error: ', err);
// you may want to show an error message on the screen.
}
}
render () {
// access the passed parameters from state
const { itemId, otherParam, loaded, singletour } = this.state;
if (loaded) {
// if once the data is loaded we can show screen
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Text>itemId: {JSON.stringify(itemId)}</Text>
<Text>otherParam: {JSON.stringify(otherParam)}</Text>
<Text>singletour: {JSON.stringify(singletour)}</Text>
<Button
title="Go to Home"
onPress={() => this.props.navigation.navigate('Home')}
/>
<Button
title="Go back"
onPress={() => this.props.navigation.goBack()}
/>
</View>
);
} else {
// while we are waiting for the data to load we could show a placeholder screen
// or we could show null. The choice is yours.
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Data not loaded</Text>
</View>
);
}
}
}
Note that componentDidMount get called after the first render occurs, this is why we have the loaded value in state. By using loaded it allows us to handle what is presented to the user rather than presenting a screen where the data hasn't finished loading.
This is clearly one possible refactor of your code. There are many other ways that it could be refactored.
Here are some great articles on setting state
https://medium.learnreact.com/setstate-is-asynchronous-52ead919a3f0
https://medium.learnreact.com/setstate-takes-a-callback-1f71ad5d2296
https://medium.learnreact.com/setstate-takes-a-function-56eb940f84b6
Related
I am starting to grasp fetch and using an API but I am trying to refer to a code source that used currencies and applying it to my own version but using an API that returns a random activity. The project I am using to better grasp my understanding is react-native expo cli
What I want it to do:
Press the search button -> returns random activity to do.
Currently, it is doing this but because my old API returns an array of objects for different currencies and my current random activity API returns just one activity I believe this is why my formatting is off but after reading and trying out different .then actions I can't seem to understand fully how to display my random activity properly instead of the multiple lines of the same activity I am currently getting I want it to display only once. (My prior question had the letters vertical which I have since fixed).
here is the link for the API i want to use https://www.boredapi.com/
here is the link for the old API https://open.er-api.com/v6/latest/USD
Thanks!
import { StyleSheet, Text, View, FlatList, SafeAreaView, Button, ScrollView } from 'react-native';
import { useEffect, useState } from 'react';
const Item = ({ item }) => {
return(
<View>
<Text>{item.value}</Text>
</View>
)
}
export default function App() {
const [data, setData] = useState([]);
var searchForActivity = () => {
fetch('http://www.boredapi.com/api/activity/')
.then((res) => res.json())
.then((json) => {
var array = Object.keys(json.activity).map((key) => ({
value: json.activity,
}));
setData(array);
});
}
useEffect(() => {
searchForActivity();
}, []);
return (
<SafeAreaView>
<ScrollView>
<View style={styles.container}>
<Text>Welcome to Activity Finder</Text>
<FlatList data = {data} renderItem={Item} />
<Button title='Search' onPress={searchForActivity} />
<StatusBar style="auto" />
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Your api call returns a single object like this
{
"activity": "Write a list of things you are grateful for",
"type": "relaxation",
"participants": 1,
"price": 0,
"link": "",
"key": "2062010",
"accessibility": 0
}
Since your data is not going to be an array you should change the data initial state to something like null.
const [data, setData] = useState(null);
You're currently looping over the keys of the activity property, which are just indices. Then map over all the keys and return an object with the property value which just has the json.activity. This does not make any sense. You can set the json directly to the state like this since you don't need an array if you only want to display one activity.
var searchForActivity = () => {
fetch("http://www.boredapi.com/api/activity/")
.then((res) => res.json())
.then((json) => {
setData(json);
});
};
By this point you can't use FlatList anymore since you data in not an array anymore, in your App you should change the FlatList with the Item component you made.
return (
<SafeAreaView>
<ScrollView>
<View style={styles.container}>
<Text>Welcome to Activity Finder</Text>
<Item item={data} />
<Button title="Search" onPress={searchForActivity} />
<StatusBar style="auto" />
</View>
</ScrollView>
</SafeAreaView>
);
Note that in your Item component you use a value property. This property does not exists on the data you get back from the api and should be changed to activity for it to show the name of the activity.
<Text>{item.value}</Text>
// change value with activity
<Text>{item.activity}</Text>
I've seen a couple of close questions, but none that really answered my question. I have the following code in React Native.
# activities add/edit screen
# ...
const [activities, setActivities] = useState([]);
useEffect(() => {
const _setup = async () => {
const temp = await fetch(...); // fetching data from server with await
setActivities(temp);
// building save button
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => _submit()}>
<Text style={{ color: '#007AFF', fontSize: 18, }}>
Save
</Text>
</TouchableOpacity>
)
});
setReady(true);
};
_setup();
}, []);
So I build the headerRight button in useEffect to pass it an local _submit function, which looks like this.
const _submit = async () => {
console.log(activities);
try {
// fetch to send data to server
} catch (e) {
showError(e);
}
};
There is a FlatList on this screen which gets dynamically extended based on user interaction. All is well until the user presses the save button in the header. It always loads the activities array from the last hot refresh/render. But the FlatList re-renders ok, the array gets extended just as I want it to be. I tried using "useCallback" on the _submit function and set the dependency to "activities", but still, the header button seems to call the "initial" _submit function. The only thing that helped was to split the useEffect into two separates and one handling the re-render of the button.
// first one to fetch data on initial mound
// ...
useEffect(() => {
// I am using react-navigation and react-native-screens 2.7.0
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => _submit()}>
<Text style={{ color: '#007AFF', fontSize: 18, }}>
Save
</Text>
</TouchableOpacity>
)
});
}, [activities]);
It works fine, but somehow feels kind of hacky... I was under the impression that normal functions (like the _submit) would get re-constructed with each re-render, which would be triggered with e.g. a new array-element being pushed to the activities, no? For completion, I add elements to activities like this.
const _addActivity = () => {
const temp = [...activities];
const initActivity = {
title: '', // will be set later via TextInput
startTime: new Date(),
endTime: new Date(),
}
temp.push(initActivity);
setActivities(temp);
}
I have two screens, one list (Flatlist) and one filter screen where I want to be able to set some filters for the list. the list screen has the states "data" and "usedFilters". When I am switching to the filters screen, the states are set as navigation parameters for react navigation and then passed via navigation.navigate, together with the onChange function, as props to the filter screen. There they are read, and the filters screen class' state is set (usually with passed filters from the list screen, if no valid filters has been passed, some are initialized).
After that the filters can be changed. If that happens, the state of the filter screen gets updated.
If then the apply button is clicked the filter screens' state is passed to the onChange function and via that back to the list screen, the onChange function updates the state "usedFilters" state of the list screen. If the cancel button is pressed null is passed to the onChange function and there is no setState call.
Setting new states for the list screen works perfectly fine. the problem is, that when i press the cancel button (or the back button automatically rendered by react navigation) the changes are kept nevertheless. That only happens if the state has been changed before. So if there has never been applied a change and hence the "usedFitlers" state of the list screen is null, this behavior does not occur. Only if I already made some changes and hence the "usedFitlers" state of the list screen has a valid value which is passed to the filters screen the cancel or go back buttons won't work as expected.
I am using expo-cli 3 and tried on my android smartphone as well as the iOS simulator. Same behavior. I looked into it with chrome dev tools as well but i simply couldn't figure out where the "usedFitlers" state was updated.
I am using react native 0.60 and react navigation 3.11.0
My best guess is that for some reason the two states share the same memory or one is pointer to the other or sth like that. (Had problems like that with python some time ago, not knowing the it uses pointers when assigning variables).
Anyone got an idea?
List Screen:
export default class ListScreen extends React.Component {
state = { data: [], usedFilters: null };
static navigationOptions = ({ navigation }) => {
let data = navigation.getParam('data')
let changefilter = navigation.getParam('changeFilter')
let currfilter = navigation.getParam('currFilter')
return {
headerTitle:
<Text style={Styles.headerTitle}>{strings('List')}</Text>,
headerRight: (
<TouchableOpacity
onPress={() => navigation.navigate('FilterScreen', {
dataset: data, onChange: changefilter, activeFilters:
currfilter })} >
<View paddingRight={16}>
<Icon name="settings" size={24} color=
{Colors.headerTintColor} />
</View>
</TouchableOpacity>
),
};
};
_onChangeFilter = (newFilter) => {
if (newFilter) {
this.setState({ usedFilters: newFilter })
this.props.navigation.setParams({ currFilter: newFilter });
} // added for debugging reasons
else {
this.forceUpdate();
let a = this.state.usedFilters;
}
}
_fetchData() {
this.setState({ data: fakedata.results },
() => this.props.navigation.setParams({ data: fakedata.results,
changeFilter: this._onChangeFilter }));
}
componentDidMount() {
this._fetchData();
}
render() {
return (
<ScrollView>
<FlatList/>
// Just data rendering, no problems here
</ScrollView>
);
}
}
Filter Screen:
export default class FilterScreen extends React.Component {
static navigationOptions = () => {
return {
headerTitle: <Text style={Styles.headerTitle}> {strings('filter')}
</Text>
};
};
state = { currentFilters: null }
_onChange = (filter, idx) => {
let tmp = this.state.currentFilters;
tmp[idx] = filter;
this.setState({ currentFilters: tmp })
}
_initFilterElems() {
const filters = this.props.navigation.getParam('activeFilters');
const dataset = this.props.navigation.getParam('dataset');
let filterA = [];
let filterB = [];
let filterC = [];
if (filters) {
// so some checks
} else {
// init filters
}
const filterElements = [filterA, filterB, filterC];
this.setState({ currentFilters: filterElements })
}
componentDidMount() {
this._initFilterElems()
}
render() {
const onChange = this.props.navigation.getParam('onChange');
return (
<ScrollView style={Styles.screenView}>
<FlatList
data={this.state.currentFilters} // Listeneinträge
keyExtractor={(item, index) => 'key' + index}
renderItem={({ item, index }) => (
<FilterCategory filter={item} name={filterNames[index]}
idx={index} onChange={this._onChange} />
)}
ItemSeparatorComponent={() => <View style=
{Styles.listSeperator} />}
/>
<View style={Layout.twoHorizontalButtons}>
<TouchableOpacity onPress={() => {
onChange(this.state.currentFilters);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('apply')} </Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
onChange(null);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('cancel')}
</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView >
);
}
}
So when I press the cancel button, null is returned to the _onChangeFilter function of the list screen. This part works, and according to console.log and the debugger, the setState is not called. But if i set a breakpoint within the else part, i can see that this.state.usedFilters has changed.
Ok after a while i figured it out. The problem was that the whole filters list was always just referenced since react native (js) seems to always use references, even when changing sub-parts of the lists.
fixed that by using lodash cloneDeep.
I have a component with render and onPress methods described below...
onCardPressed(event) {
console.log(this.props);
const { data } = this.props;
console.log(event, data);
}
render() {
const { data } = this.props;
return (
<TouchableOpacity
onPress={this.onCardPressed}
>
<Container style={{ elevation: 5 }}>
<SectionTitle>
This is a
{` ${data.city} `}
card
</SectionTitle>
</Container>
</TouchableOpacity>
);
}
In this example, the card will properly display This is a London card, but in the onPress method this.props returns undefined.
How can I access the this.props object for evaluation?
You can fix this by two way. The argument in favour of adding these lines to the constructor is so that the new bound functions are only created once per instance of the class. You could also use
onPress={this.onCardPressed.bind(this)}
or (ES6):
onPress={() => this.onCardPressed()}
I am trying to build a lunch picker app that allows user to add their own menu. I want to save user data into array by using AsyncStorage. However, my value returns nothing even though the array has values. Below is my code.
//Main screen
class HomeScreen extends React.Component {
//initial
constructor(props) {
super(props);
this.state = {
isReady: false,
myMenu: '????',
menutext: '',
randomArray: ['a', 'b', 'c'],
visibility: false,
};
}
_loadMenu = async () => {
try{
const loadMenu = await AsyncStorage.getItem("menuInStorage")
const parsedLoadMenu = JSON.parse(loadMenu)
const myReturn = [...this.state.randomArray, parsedLoadMenu]
this.setState({randomArray: myReturn})
}
catch(err){
alert(err)
}
}
//get input from textinput field and add to array
addMenu = newMenu => {
//...
this._saveMenu(this.state.randomArray)
};
_saveMenu = (saving) => {
const saveMenu = AsyncStorage.setItem("menuInStorage", JSON.stringify(saving))
}
//control modal
setModalVisibility(visible) {
this.setState({visibility: visible});
}
//UI
render() {
return (
<View style={styles.mainContainer}>
<View style={[styles.container, {flexDirection: 'row', justifyContent: 'center'}]}>
<TextInput
style={{ height: 40, fontSize: 20, paddingLeft: 15, textAlign: 'left', width: 250, borderBottomColor: '#D1D1D1', borderBottomWidth: 1 }}
placeholder=".."
onChangeText={menutext => this.setState({ menutext })}
value={this.state.menutext}
/>
<Button
title=".."
onPress={() => this.addMenu(this.state.menutext)}
buttonStyle={{width:100}}
backgroundColor="#2E282A"
/>
</View>
<Text>{'\n'}</Text>
<Button
onPress={() => this.setModalVisibility(true)}
title=".."
buttonStyle={{width: 150}}
backgroundColor="#2E282A"
/>
</View>
<Modal
onRequestClose={() => this.setState({ visibility: false })}
animationType={'slide'}
transparent={false}
visible={this.state.visibility}
>
<View style={[styles.modalContainer, {marginBottom: 100}]}>
<Text style={[styles.text, { fontWeight: 'bold', padding: 20, backgroundColor: '#9090DA', borderBottomColor: '#5C5C8B',
borderBottomWidth: 1,}]}>
{'<'}List will be here{'>'}
</Text>
<ScrollView style={{height: "94%"}}>
<View style={styles.row}>{this.state.randomArray}</View>
</ScrollView>
<Button
buttonStyle={{justifyContent: 'center', marginTop: 5}}
backgroundColor="#2E282A"
onPress={() => this.setModalVisibility(!this.state.visibility)}
title="Close"
/>
</View>
</Modal>
</View>
);
}
}
How the app supposed to work is, when user clicks a button, the modal shows all data in array called 'randomArray'. After user added their custom text, it should be added at the end of the randomArray. I want to save this data to the disk and load from the disk when the app is launched. At this moment, I can load array data, but it doesn't keep user data. My current code returns nothing. I need your help. Thanks.
It looks like the logic in _loadMenu is slightly incorrect on this line:
const myReturn = [...this.state.randomArray, parsedLoadMenu]
If I understand correctly, you're expecting parsedLoadMenu to be a value of type Array. The line above will basically append the value parsedLoadMenu to the resulting array stored in myReturn - in the case of your code, this will mean the last item of myReturn will be an array, which would be incorrect from what I see in your code. Consider updating this line as shown:
/*
Add ... before parsedLoadMenu to concatenate the two arrays in myReturn
*/
const myReturn = [...this.state.randomArray, ...parsedLoadMenu]
By adding the ... as shown, this causes the two arrays this.state.randomArray and parsedLoadMenu to be concatenated together in myReturn. It would also be worth checking the parse result from JSON.parse() to ensure that it is an array before attempting this concatenation:
_loadMenu = async () => {
try{
const loadMenu = await AsyncStorage.getItem("menuInStorage")
let parsedLoadMenu = JSON.parse(loadMenu)
/*
Consider an additional check here to ensure the loaded data is of
correct Array type before proceeding with concatenation
*/
if(!Array.isArray(parsedLoadMenu)) {
parsedLoadMenu = [];
}
/* Concatenate the two arrays and store result in component state */
const myReturn = [...this.state.randomArray, ...parsedLoadMenu]
this.setState({randomArray: myReturn})
}
catch(err){
alert(err)
}
}
Also, consider revising the addMenu logic, so that the entire array of menu items in your is persisted to AsyncStorage rather than the newly added menu item only, as you are currently doing:
addMenu = (newMenu) => {
/*
Persist current randomArray with newMenu item appended
*/
this._saveMenu([...this.state.randomArray, newMenu])
};
Hope this helps!