I'm working on a tours app which has a ratings component in the details screen, whenever an user votes it gets saved using a device token as ID, i need to make it work in a way that when i go back to the tours list and then click the on tour i already voted (or another one i haven't) it needs to show me the previous rating i gave, say if i gave it 3 stars i want to have those 3 stars painted the next time i click on said tour, or show 0 if i haven't rated yet. This is what im currently doing:
const [defaultRating, setdefaultRating] = useState(0);
const starImageFilled = 'https://raw.githubusercontent.com/AboutReact/sampleresource/master/star_filled.png';
const starImageCorner = 'https://raw.githubusercontent.com/AboutReact/sampleresource/master/star_corner.png';
const CustomRatingBar = () => {
return (
<View style={styles2.customRatingBarStyle}>
{maxRating.map((item, key) => {
return (
<TouchableOpacity
activeOpacity={0.7}
key={item}
onPress={() => {
setdefaultRating(item);
attempVotation(detail, item)
previousRate.current = item;
console.log(previousRate.current)
}}>
<Image
style={styles2.starImageStyle}
source={
item <= defaultRating
? { uri: starImageFilled }
: { uri: starImageCorner }
}
/>
</TouchableOpacity>
);
})}
</View>
);
}
const attempVotation = (tour, votacion) => {
setIsLoadingVote(true);
wretch(`${Config.API_URL}${Endpoints.tourVotacion}`)
.post({
"TourId" : tour.Id,
"DeviceId" : deviceToken,
"Clasificacion" : votacion
})
.json((json) => {
setIsLoadingVote(false);
Alert.alert('Informacion', json, [
{text: 'OK'},
]);
})
.catch((error) => {
Alert.alert('Error', 'There was an error saving your rating, try again.', [
{text: 'OK'},
]);
setIsLoadingVote(false);
});
};
Right now everything works fine, i just need to do what i previously mentioned.
Related
well first i need to display the existing image that comes from API in react native.
then i need to update the existing image and replace with new picture.
Code:
<FlatList
data={filePath}
keyExtractor={(item, index) => index}
renderItem={({item}) => {
setImageName(item.fileName);
setImageType(item.type);
setImageUri(item.uri);
return (
<View>
<Image source={{uri: item.uri}} style={styles.imageStyle} />
</View>
);
}}
/>
button where i set my new picture
<GlobalButton
onPress={() => {
chooseFile('photo');
}}
text={'Add Image'}
/>
const chooseFile = type => {
let options = {
mediaType: type,
maxWidth: 300,
maxHeight: 550,
quality: 1,
};
launchImageLibrary(options, response => {
if (response.didCancel) {
showError('User cancelled camera picker');
return;
} else if (response.errorCode == 'camera_unavailable') {
showError('Camera not available on device');
return;
} else if (response.errorCode == 'permission') {
showError('Permission not satisfied');
return;
} else if (response.errorCode == 'others') {
showError(response.errorMessage);
return;
}
setFilePath(response.assets);
});
};
i get the image uri from API . i have showed it in return but it shows me two picture the existing one and new one
well first you need to make a state and set it to true like this one
const [newImageSelected, setNewImageSelected] = useState(false)
when you pick a new image from image picker then set this state to
true
const chooseFile = type => {
let options = {
mediaType: type,
maxWidth: 500,
maxHeight: 500,
quality: 1,
};
launchImageLibrary(options, response => {
if (response.didCancel) {
showError('User cancelled camera picker');
return;
} else if (response.errorCode == 'camera_unavailable') {
showError('Camera not available on device');
return;
} else if (response.errorCode == 'permission') {
showError('Permission not satisfied');
return;
} else if (response.errorCode == 'others') {
showError(response.errorMessage);
return;
}
setFilePath(response.assets);
setNewImageSelected(true);
});
};
then in return write set the condition if image is already existed
then it only shows the one picture which you are getting from the. and
when you select the new image the existing image replaced with the new
one check the below code maybe it helps you:
{newImageSelected ? (
<FlatList
data={filePath}
keyExtractor={(item, index) => index}
renderItem={({item}) => {
setImage(item.fileName);
setImageUri(item.uri);
setImageType(item.type);
return (
<View>
<Image
source={{uri: item.uri}}
style={styles.imageStyle}
/>
</View>
);
}}
/>
) : (
<Image
source={{uri: `existing image URL`}}
style={styles.imageStyle}
/>
)}
To update your screen with your data , you need to use state
To understand how it works first refer this link so you can understand how it works and how you can use it
https://reactnative.dev/docs/state.html
After that you can check how flatlist work because as per your code , you are not much aware with react native ..
FlatList accept array as data not object
here you can refer documentation
https://reactnative.dev/docs/flatlist
What I'm trying to do is to display in a list all the clients.
When there is no client in the list, it is another view that is displayed which says no client exists.
The initial value of the state its an empty array.
So when the axios.get() method gets the data from the backend, two times its called the initial value(which is an empty array), and after its filled with the client list that comes from backend.
const [clientList, setClientList] = useState([])
useEffect(() => {
axios
.get(`api/${phone}`)
.then(setClientList(response.data.data))
})
.catch((error) => console.log(error));
}
});
}, [clientList]);
console.log(clientList)
return(
{clientList.length > 0? (
<View>
<FlatList
data={clientList}
renderItem={(item) => {
let client= item.item.fullName;
return (
<View >
<Text>{client}</Text>
/>
</View>
)
}}
/>
</View>
) : (
<Text> No Client List </Text>
)}
)
When I console the clientList it will show:
clientList[]
clientList[]
clientList[
{
fullName: John Doe
},
{
fullName: Nick Smith
}
]
The first two empty arrays (that comes from the initial value of the useState) it will show the No Client List every time the user goes to the client list screen or reload the screen.
How can I prevent showing the <Text> No Client List </Text> when the clientList has clients on the array?
you can add a new state isLoading which will be true by default to handle the clientList initial empty array case.
const [isLoading, setIsLoading] = useState(true)
...
useEffect(() => {
axios
.get(`api/${phone}`)
.then(() => {
setIsLoading(false)
setClientList(response.data.data)
})
.catch(() => {
setIsLoading(false)
});
}, []);
...
// you can conditionally return UI based on `isLoading` or you can use `ListEmptyComponent` prop from FlatList along with `isLoading`
if(isLoading) {
return(<Text>loading</Text>)
}
// remaining code
return(
...
I'm using React Native (0.68) and Firebase RTDB (with the SDK, version 9), in Expo.
I have a screen that needs to pull a bunch of data from the RTDB and display it in a Flatlist.
(I initially did this without Flatlist, but initial rendering was a bit on the slow side.)
With Flatlist, initial rendering is super fast, huzzah!
However, I have an infinite loop re-render that I'm having trouble finding and fixing. Here's my code for the screen, which exists within a stack navigator:
export function GroupingsScreen () {
... set up a whole bunch of useState, database references (incl groupsRef) etc ...
onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
if (loaded == false) {
console.log('--start processing')
setLoaded(true);
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
console.log('--done processing')
setGroupsArray(newObject)
}
});
.... more stuff excerpted ....
return (
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)};
I'm using loaded/setLoaded to reduce re-renders, but without that code, RN immediately dumps me out for excessive re-renders. Even with it, I get lots of extra renders.
So...
Can someone point me at what's triggering the rerender? The database is /not/ changing.
Is there a better way to get RTDB info into a Flatlist than the code I've written?
I have some code that actually does change the database. That's triggering a full rerender of the whole Flatlist, which is visibly, painfully slow (probably because parts are actually rendering 10x instead of once). Help?
For completeness, here's the OneItem code, so you can see just how complex my Flatlist items are:
const OneItem = (data) => {
// console.log('got data',data)
return (
<View style={[styles.rowView, { backgroundColor: data.sku?'white': '#cccccc'}]} key={data.name}>
<TouchableOpacity style={styles.nameView} onPress={() => {
navigation.navigate('AddEditItemScreen', {purpose: 'Grouping', itemname: data.name, parent: data.parent, mode: 'fix'})
}}>
<View style={styles.nameView}>
<Text style={styles.itemtext}>{data.name}</Text>
{data.sku? null: <Text>"Tap to add SKU."</Text>}
{data.size?<Text>{data.size} </Text>: <Text>no size</Text>}
</View>
</TouchableOpacity>
<View style={styles.buttonView}>
<Button style={styles.smallButton}
onPress={() => { changeQuant(data.quantity ? data.quantity - 1 : -1, data.parent + '/ingredients/' + data.name) }}
>
{data.quantity > 0 ? <Text style={[styles.buttonText, { fontSize: 20 }]}>-</Text>
:<Image source={Images.trash} style={styles.trashButton} />}</Button>
<Text style={styles.quantitytext}>{data.quantity}</Text>
<Button style={styles.smallButton}
onPress={() => {
changeQuant(data.quantity? data.quantity +1 : 1, data.parent+'/ingredients/'+data.name)}}>
<Text style={[styles.buttonText, {fontSize: 20}]}>+</Text></Button>
</View>
</View>
)
};```
I worked out how to stop the rerender (question #1). So, within my Screen functional component, I needed to make another function, and attach the state hook and useEffect to that. I'm not totally sure I understand why, but it gets rid of extra renders. And it's enough to get #3 to tolerable, although perhaps not perfect.
Here's the new code:
export function GroupingsScreen () {
... lots of stuff omitted ...
function JustTheList() {
const [groupsArray, setGroupsArray] = useState([])
useEffect(() => {
const subscriber = onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
setGroupsArray(newObject)
})
return () => subscriber();
}, [])
return(
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)
}
And then what was my return within the main functional screen component became:
return (
<JustTheList />
)
I'm still very interested in ideas for improving this code - am I missing a better way to work with RTDB and Flatlist?
I have a React Native Flatlist that only re-renders when its data has changed.
I give it the following data (as prop):
const posts = [
{
...post1Data
},
{
...post2Data
},
{
...post3Data
},
{
...post4Data
},
{
...post5Data
},
]
And here is my FlatList renderItem:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
How can I add an AdMob ad between the FlatList data with a probability of 5% without skiping any data in the posts array?
I have tried this:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
if (Math.random() < 0.05) return <Ad ... />
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
But this causes 2 problems:
Some items from data are skipped (not returned)
When the flatlist re-renders (because of some of its props changes) the ads might disappear (there is a chance of 95%).
Any ideas? Should I render the ads randomly in the footer of my Card component like this?
const Card = memo ((props) => {
...
return (
<AuthorRow ... />
<Content ... />
<SocialRow ... /> {/* Interaction buttons */}
<AdRow />
)
}, (prevProps, nextProps) => { ... });
const AdRow = memo(() => {
return <Ad ... />
}, () => true);
I am not really sure about this option, it works but it could violate the admob regulations (?) (because I am adapting the ad to the layout of my card component)
I would appreciate any kind of guidance/help. Thank you.
I'm not sure if you ever found a solution to this problem, but I accomplished this by injecting "dummy" items into the data set, then wrapping the renderItem component with a component that switches based on the type of each item.
Assuming your flatlist is declared like this:
<FlatList data={getData()} renderItem={renderItem}/>
And your data set is loaded into a variable called sourceData that is tied to state. Let's assume one entry in your sourceData array looks like this. Note the 'type' field to act as a type discriminator:
{
"id": "d96dce3a-6034-47b8-aa45-52b8d2fdc32f",
"name": "Joe Smith",
"type": "person"
}
Then you could declare a function like this:
const getData = React.useCallback(() => {
let outData = [];
outData.push(...sourceData);
// Inject ads into array
for (let i = 4; i < outData.length; i += 5)
{
outData.splice(i, 0, {type:"ad"});
}
return outData;
}, [sourceData]);
... which will inject ads into the data array between every 4th item, beginning at the 5th item. (Since we're pushing new data into the array, i += 5 means an ad will be placed between every 4th item. And let i = 4 means our first ad will show after the 5th item in our list)
Finally, switch between item types when you render:
const renderItem = ({ item }) => (
item.type === 'ad'
?
<AdComponent ...props/>
:
<DataComponent ...props/>
);
I first make an Ajax call (to an API) which provides me some data, a list of achievements (array of objects). I would like to loop through this array, show the first achievement as a Modal and on click of a button close the modal then show the next one (next achievement) and so on.
Ajax call providing the data:
getAchievements = () => {
fetch(url + '/achievements', {
method: 'get',
headers: {
Accept: 'application/json',
'Content-type': 'application/json'
}
})
.then((data) => data.json())
.then((data) => {
this.props.addData({
achievements: data.achievements
})
if(this.props.store.achievements.length > 0) {
this.setState({
showAchievementModal: true
})
}
})
.catch((error) => {
console.error(error)
})
}
Here I show the modals:
render() {
return (
{this.state.showAchievementModal &&
<Modal
animationType={'fade'}
visible={this.props.store.isModalAchievementVisible}
>
{this.props.store.achievements.map((data,index)=>{
return(
<View key={index}>
<View style={styles.container}>
<Text>{data.title}</Text>
<Text>{data.description}</Text>
<TouchableOpacity onPress={this.closeModal}>
<Text>Collect</Text>
</TouchableOpacity>
</View>
</View>
)
})}
</Modal>
}
)
}
At the moment all the Modals open at the same time. How could I open them one after the other after clicking the Collect button?
This is the updated version of my code that works:
Initialising activeModalIndex in the constructor:
constructor(props) {
super(props)
this.state = {
activeModalIndex: 0
}
}
Get achievements:
getAchievements = () => {
if(this.props.store.achievements.length > 0) {
this.setState({
showAchievementModal: true,
activeModalIndex: 0,
})
}
}
Render function:
render() {
return this.props.store.achievements.map((data,index) => this.state.activeModalIndex === index &&
<Modal>
<View key={index}>
<View style={styles.container}>
<Text>{data.title}</Text>
<Text>{data.description}</Text>
<TouchableOpacity onPress={this.closeModal}>
<Text>Collect</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
)
}
Close Modal:
closeModal = () => {
const maxIndex = this.props.store.achievements.length - 1
const currentIndex = this.state.activeModalIndex
const isLastModal = currentIndex === maxIndex
const newIndex = isLastModal? -1: currentIndex +1
this.setState({
activeModalIndex: newIndex
})
}
The problem is that you have multiple Modals on your page and they all use the same boolean to check if they should be rendered. Initially, showAchievementModal is set to true, so all modals are rendered. Furthermore, after you set showAchievementModal to false in closeModal, it will permanently stay false, so no additional modals will get rendered.
render() {
return (
{this.state.showAchievementModal &&
<Modal
...
</Modal>
}
)
}
Instead of showAchievementModal you should be keeping track of index of active modal. So, after you fetch the list of achievements from your API, set the activeModalIndex to 0. After the user dismisses this first modal, set the activeModalIndex to 1 inside the closeModal method, then set it to 2 after the second modal is closed and so on.
Now, for every modal to correspond to a single achievement, we must map each element of the achievements array to a single Modal and conditionally render it only if its corresponding index is the active one.
render() {
const achievements = this.props.store.achievements;
const { activeModalIndex } = this.state;
return achievements.map((data, index) => activeModalIndex === index &&
<Modal key={index}>
<View>
<View style={styles.container}>
<Text>{data.title}</Text>
<Text>{data.description}</Text>
<TouchableOpacity onPress={this.closeModal}>
<Text>Collect</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
)
}
When the users dismisses the currently active modal, simply increment the index of the active modal and the next modal will appear instead of the current one. If the new incremented value is equal or larger than the array length, nothing will get rendered, so no need to check for max index value before setting new state.
closeModal = () => {
this.setState(previousState => ({
activeModalIndex: previousState.activeModalIndex + 1,
}))
}
Also, please read about the dangers of setting index as key when rendering lists. If you happen to need ordering achievements by some value/priority and users can retrieve multiple pages of their achievements, it might cause rendering wrong components.