How do I get my api data to display as a single item instead of printing vertically letter by letter? - react-native

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>

Related

React Native - Variable link using a prop

I'm making an app with some products that I got from my Wordpress database. On the homescreen, I have an overview of all the products, each in a tile. I want to be able to put a button in each tile, which links to the specific product page. But, since it works with a component, I need to be able to do this with a prop. And, if possible, based on the title of the API.
This is my code for the screen with all the products:
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, FlatList, Image, Button } from 'react-native';
import SuitcaseItem from '../components/SuitcaseItem';
const AllSuitcasesScreen = ({ navigation }) => {
const [suitcases, setSuitcases] = useState([]);
const getSuitcases = async () => {
try {
const response = await fetch("https://evivermeeren.com/wp-json/wp/v2/posts?categories=59", {
}
)
const json = await response.json();
console.log(json);
setSuitcases(json);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
getSuitcases();
}, []);
return (
<View style={styles.screen}>
<View style={styles.flexbox2}>
<Text style={styles.products}>Onze koffers</Text>
<View style={styles.shoppingcart}>
<Image
style={styles.icon}
source={{uri: 'https://cdn-icons-png.flaticon.com/512/1413/1413908.png'}}
/>
<Text style={styles.number}>0</Text>
</View>
</View>
<View style={styles.list}>
<FlatList
data={suitcases}
renderItem={({ item }) => (
<SuitcaseItem
title={item.title.rendered}
imageUri={{uri: 'https://www.samsonite.be/on/demandware.static/-/Sites/default/dw851ab6f0/images/misc/sams_share-image.jpg'}}
desc={item.slug}
buttonText={item.title.rendered}
/>
)}
/>
</View>
</View>
);
}
export default AllSuitcasesScreen;
And this is the result:
Now, when I click the black button, I go to the page 'Evo L', which I also made. This is the button that I use:
<Pressable style={styles.seeProduct} onPress={() => navigation.navigate("Evo L")}>
<Text style={styles.text}>Bekijk product: {props.buttonText}</Text>
</Pressable>
This is in another file, the 'SuitcaseItem'.
So, I should be able to put something like navigation.navigate("props.buttonNav") with buttonNav = {item.title.rendered} so it goes to the page Evo L if I click on that one and then Evo M when I click on that tile and so one. Does anyone have an idea?
You can pass props to a screen. See this excellent official documentation for React Navigation on passing props.
-> Make a generic item detail screen like ItemDetail (instead of Evo L).
-> Modify the navigation.navigate("props.buttonNav") to:
navigation.navigate("ItemDetail", {itemTitle: props.buttonText})
You can access these props in the ItemDetail screen as:
function ItemDetail({ navigation, route }) {
return(
<Text>route.params.itemTitle</Text>
)
}

React Native List with Map Method add New Item below selected Item

React Native List with Map Method
What I want to achieve,
I want when click item then a new Item (I preferer add a new custom View) is added below the Selected item.
Expo Snack code>
https://snack.expo.dev/#stefanosalexandrou/tenacious-french-fries
Since you are changing the background of the selected item, it is necessary that you update the ID's of every item in the list, for otherwise inserting elements will break this functionality. Furthermore, you need to add a state for for otherwise you cannot trigger a UI change
You could implement the desired behaviour as follows.
const [selectedId, setSelectedId] = useState(null);
const [data, setData] = React.useState(persons)
function handleOnPress(idx) {
setSelectedId(idx)
const first = data.slice(0, idx + 1);
const second = data.slice(idx + 1).map(p => ({...p, id: Number(p.id) + 1}));
setData([...first, {id: idx + 2, name: "Whatever new iten"}, ...second])
}
return (
<View style={styles.container}>
<ScrollView>
<View>
{data.map((person, index) => {
const backgroundColor = index === selectedId ? "#6e3b6e" : "#f9c2ff";
return (
<TouchableOpacity
onPress={() => handleOnPress(index)}
style={{
padding:20,
backgroundColor: backgroundColor,
marginBottom:20,
}}
>
<Text>{person.name}</Text>
</TouchableOpacity>
);
})}
</View>
</ScrollView>
</View>
);
Use slice in order to split the array into two parts. Use map for updating the id attribute of the elements in the second array. Finally, combine both parts but insert a new element between them.
Here is an updated snack.
There are some points to consider and I'll list them here, before providing an idea of a solution:
React Native provides performance-optimized components that handle list rendering named <FlatList />/<SectionList />. Use those components instead of .map() for rendering component lists
You'll need to create an internal state for your list to be changed
You need to provide a key prop when rendering a list of components using .map() or other Array methods
With minimal changes to your provided code, you can create a state to store the list and when the item is pressed you can insert a new item inside this list:
import React, { useState } from "react";
import { Text, View, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
const people = [/* your list */];
export default function App() {
const [peopleList, setPeopleList] = useState(people)
const [selectedId, setSelectedId] = useState(null);
return (
<View style={styles.container}>
<ScrollView>
<View>
{list.map((person, index) => {
return (
<TouchableOpacity
onPress={() => {
setSelectedId(person.id)
const newPerson = {...person}; // The new item
setPeopleList((prevList) => [...prevList.slice(0,index + 1), newPerson, ...prevList.slice(index + 1)])
}}
style={{
padding:20,
backgroundColor: backgroundColor,
marginBottom:20,
}}
>
<Text>{person.name}</Text>
</TouchableOpacity>
);
})}
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding:20
}
});
Sandbox with working code: https://snack.expo.dev/5rvTbrEvO

How can I display 30 pages of text in a (scrolling) screen

I want to display 30 pages of text on a screen. I've tried ScrollView and FlatList but I get a white screen. Only when I try with ScrollView to display only 2 pages, works fine.
I do not want to use a WebView, because I would like to have all data in the app (no internet connection needed).
Here is what I've already tried:
With FlatList:
I have a text.js as a model, which I use to create a Text Object in an array, which I then use as data for the FlatList. For the renderItem function (of FlatList) I use a TextItem to display the text.
text.js
function Text(info) {
this.id = info.id;
this.text = info.text;
}
export default Text;
LongTextModule.js
import Text from '../../models/text';
export const LONGTEXT = [
new Text({
id:'text_1',
text:`.....longtext....`
})
]
TextItem.js
const TextItem = (props) => {
return (
<View style={styles.screen} >
<Text style={styles.textStyle}>{props.longText}</Text>
</View >
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
textStyle: {
justifyContent: 'flex-start',
alignItems: 'flex-start',
fontFamily: 'GFSNeohellenic-Regular',
fontSize: 20,
padding: 10,
}
});
TextDetailScreen.js
const TextDetailScreen = (props) => {
const renderText = data => {
return <TextItem longText={data.item.text} />
}
return <FlatList
data={LONGTEXT}
keyExtractor={(item, index) => item.id}
renderItem={renderText}
/>
};
I think it's needless to show the code with ScrollView, since ScrollView is only for a small list.
I even tried to render the longText like this in the screen.
Without the ScrollView I get the first portion, but with ScrollView a white screen.
const TextDetailScreen = (props) => {
return (
<ScrollView>
<Text> ...longText...</Text>
</ScrollView>
);
};
I'm sure there is a way to display a lot of pages of text on a screen?
But how?
Thank you :)
It seems not to be an unknown Issue, I've also read from time to time about this issue.
But not to use Webview, because you wan't to have all Data in your app - don't have to be an Argument against Webview. With WebView, you also can display Data from your App-Storage.
Example:
<WebView style={styles.myStyle} source={{html: `<p style="font-size:48px">${longtext}</p>`}} />

React native updates state "on its own"

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.

How to pass data to the render method in react-native

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