I'm developing an app which has to fetch new orders from the Firestore database, I used componentDidMount to refresh the screen every 10 seconds and launch the fetchNewOrders function, if new orders are available, the function should push that object into the state array newOrder, and display the orders in the FlatList below. When I start the code it returns the error TypeError: undefined is not an object (evaluating 'item.id'), I also wrote the example of an array I'm fetching from the database.
Screen
export default class Received extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
newOrder: [],
};
}
async componentDidMount() {
this.updateTimer = setInterval(() => {
this.fetchNewOrders();
}, 10000);
}
fetchNewOrders = async () => {
const querySnapshot = await getDocs(collection(db, path));
if(querySnapshot.length !== 0) {
querySnapshot.forEach((doc) => {
let array = this.state.newOrder;
const data = doc.data().order;
data.map(({obj, id}) => {
const filter = array.find(c => c.id === id);
if (filter == undefined) {
array.push(obj)
this.setState({ newOrder: array })
}
})
})
}
}
render() {
return (
<View>
<FlatList
data={this.state.newOrder}
keyExtractor={item => item.id}
renderItem={({ item }) => {
return (
<TouchableOpacity>
<Text>{item.serviceRequested}</Text>
<View>
<Tex>${item.total}</Text>
</View>
</TouchableOpacity>
)
}}
/>
</View>
)
}
}
data (new order)
Array [
Object {
"date": "Mon Feb 28 2022 11:24:14 GMT-0500 (EST)",
"id": 0.9436716663143794,
"instructions": "",
"order": Array [
/////////////
],
"paymentType": "Cash",
"serviceRequested": "Delivery",
"total": 10.4,
},
]
setState is an async function that schedules a render with new state. It should only be called once per render, not in a loop.
const append = [];
querySnapshot.forEach((doc) => {
const data = doc.data().order;
// Is there bad data without ids?
data.filter(c => c.id && !this.state.newOrder.some(no => no.id === c.id))
.forEach((d) => append.push(d));
});
// Create the new state, then set it once.
this.setState({ newOrder: [...this.state.newOrder, ...append]});
I would suggest filtering your data (this.state.newOrder) first. This would make it so that you only display items that have ids.
Suggested Change:
<FlatList
data={this.state.newOrder.filter((order)=>order.id)}
keyExtractor={item => item.id}
renderItem={({ item }) => {
return (
<TouchableOpacity>
<Text>{item.serviceRequested}</Text>
<View>
<Tex>${item.total}</Text>
</View>
</TouchableOpacity>
)
}}
/>
Above is code to fix this issue as described, but I would suggest that you make sure the Firestore only sends data that has id's if possible. Obviously, this may be out of your hands, but I would make sure that the Firestore is giving you reliable data, as it could cause more problems down the road.
Related
I have a flatlist that receives eventIds as data. On each renderItem call I want to find the corresponding event-object to each eventId on firebase and render this data. (E.g. title or image from this event)
This is the function to get a single event from firestore based on the eventId:
export async function getSingleEventDataFireStore(eventId) {
const db = firebase.firestore();
const eventFirestoreDoc = db.collection("events").doc(eventId);
const doc = await eventFirestoreDoc.get();
if (!doc.exists) {
console.log("No such Event!");
} else {
console.log("Event data fetched!");
return await doc.data();
}
}
this is where the flatlist is displayed:
const EventScreen = () => {
const [event, setEvent] = useState([]);
const [eventFetched, setEventFetched] = useState(false);
const eventIds = [{id:'1', eventId: 'jaelfmk130'}, {id:'2', eventId: '1jlk335n1'}]
const renderItem = ({ item }) => {
if (!eventFetched) {
firestore.getSingleEventDataFireStore(item.eventId).then((eventItem) => {
setEvent(eventItem);
setEventFetched(true);
});
}
setEventFetched(false)
return ( <View><Text>{event.title}</Text></View> )
}
return (
<View>
<FlatList
data={eventIds}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
</View> )
}
This ends up in an infinite loop, because I set eventFetched to false again during renderItem. Otherwise, if I remove setEventFetched(false) it only renders one item.
How can I set eventFetched to false after the render of each item without ending up in a loop?
I'm calling an Api to get data but the data is really heavy. I'm wondering if i'm calling it in right place inside useEffect or should i call it somewhere else. I've put the console.log to check but the number of console.log exceeded the number of objects i have in the API. My code is :
const ProductsList = () => {
const [products, setProducts] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
getProducts().then((response) => {
if (isMounted) {
console.log('im being called');
setProducts(response);
setLoading(false);
}
});
return () => { isMounted = false; };
}, [products]);
return (
<View style={styles.container}>
{isLoading ? <ActivityIndicator /> : ((products !== [])
&& (
<FlatList
data={products}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return (
<Item
style={{ marginLeft: 35 }}
name={item.name}
date={item.date}
address={item.adress}
/>
);
}}
/>
)
)}
</View>
);
};
It looks like your effect goes round in a circle:
On each render the effect will look at products to see if it has changed.
If it has changed it will call your effect which fetches new products.
When you get new products you update your products state.
This causes the effect to run again.
You probably only want to run that effect once, when the component mounts. In which case you can simply write:
useEffect(() => {
getProducts().then((response) => {
setProducts(response);
setLoading(false);
});
}, []); // <-- note the empty array, which means it only runs once when mounted
If that single API call is too heavy, then you need to look more at things like pagination in your requests, or modifying the response to return only the data you really need. But that's outside the scope of this question, I think.
Let me know if you have any questions.
I know that extraData is used to update flatlist when it changes but somehow it does not work in my case. I know I am doing something wrong but I need advice to figure that problem out.
Here is my flatList:
<FlatList
data={this.state.data}
extraData={this.state.data}
renderItem={this.renderPost}
keyExtractor={(item, index) => index.toString()}
onEndReached={this.loadMorePosts}
onEndReachedThreshold={0.5}
ListFooterComponent={this.renderFooter}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={this.state.loading}
onRefresh={this.loadNewerPosts}
/>
}
/>
and here is my deleteRequest that should remove one item from this.state.data:
deletePost = (index) => {
console.log(this.state.data.length);
let data = this.state.data;
data.splice(index, 1);
this.setState({ data: data }, () => {
console.log(this.state.data.length);
});
};
I even tried to put refresh in state and to change it every time I delete item and put it as extraData but nothing happens. What am I doing wrong?
This.state.data.length is changing so the data changes but flatList do not re-renders.
Do something like
deletePost = (index) => {
....
let data = [...this.state.data]; // here is a logic
data.splice(index, 1);
this.setState({ data: data }, () => {
console.log(this.state.data.length);
});
}
If you want to use the Pull To Refresh feature try something like this:
refreshFlatlist = () => {
this.setState(
{
refresh: true,
},
() => this.getDataHandler() // whatever updates your dataArray displayed in the list
);
this.setState({
refresh: false,
});
};
Now the flatlist RefreshComponent looks like this:
<FlatList
refreshControl={
<RefreshControl
refreshing={this.state.refresh}
onRefresh={this.refreshFlatlist}
/>
}
extraData={this.state.refresh}
data={this.state.data}
keyExtractor={(item, index) => item.id.toString()}
renderItem={({ item }) => ( ...
Don't use splice. Try below snippet it's working as expected.
deletePost = index => {
const newData = this.state.data.filter((item, i) => i !== index);
this.setState({
data: newData,
});
};
render(){
return (
<FlatList
data={this.state.data}
extraData={this.state.data}
renderItem={this.renderItem}
/>
);
}
deletePost = (index) => {
let data = this.state.data;
data.splice(index, 1);
this.setState({ data: [] }, () => {
this.setState({ data: data });
});
};
Thanks all. With your solutions I came up with this idea and it works perfectly.
I'm making two calls from an API. I want to display the top results for airing shows and top tv shows. I have all of the data being returned from both API calls, but my code isn't efficient. I'd like to somehow take my returned data and display it in a single component (TopAnime) that will then map and return the information provided.
I figured reduce would be the best route, but I'm fumbling at this point. My thought process was to reduce the returned data from the API into an array. Take that reduced array and pass it as my new state and then have my component display it without having to write duplicate code. Both topTv and topAIring are showing because I've written the component twice, but it's clearly not best practice to repeat code.
class HomePage extends Component {
state = {
topTv: [],
topAiring: []
}
async getData() {
const api = "https://api.jikan.moe/v3"
const urls = [
`${api}/top/anime/1/tv`,
`${api}/top/anime/1/airing`
];
return Promise.all(
urls.map(async url => {
return await fetch(url) // fetch data from urls
})
)
.then(responses => // convert response to json and setState to retrieved data
Promise.all(responses.map(resp => resp.json())).then(data => {
console.log("data", data)
// const results = [...data[0].top, ...data[1].top]; // data from TV & data from airing
const reduceResults = data.reduce((acc, anime) => {
return acc + anime
}, [])
console.log('reduce', reduceResults);
const tvResults = data[0].top // data from TV
const airingResults = data[1].top // data from airing
this.setState({
topTv: tvResults,
topAiring: airingResults
});
})
)
.catch(err => console.log("There was an error:" + err))
}
componentDidMount() {
this.getData();
}
render() {
return (
<HomeWrapper>
<h2>Top anime</h2>
<TopAnime>
{this.state.topTv.map((ani) => {
return (
<div key={ani.mal_id}>
<img src={ani.image_url} alt='anime poster' />
<h3>{ani.title}</h3>
</div>
)
}).splice(0, 6)}
</TopAnime>
<h2>Top Airing</h2>
<TopAnime>
{this.state.topAiring.map((ani) => {
return (
<div key={ani.mal_id}>
<img src={ani.image_url} alt='anime poster' />
<h3>{ani.title}</h3>
</div>
)
}).splice(0, 6)}
</TopAnime>
</HomeWrapper>
)
}
}
Since the response from API contains a flag called rank you can use the Array.prototype.filter to only show shows ranked 1-6.
Working demo here
import React, { Component } from "react";
import { TopAnime } from "./TopAnime";
export class HomePage extends Component {
state = {
topTv: [],
topAiring: []
};
async getData() {
const api = "https://api.jikan.moe/v3";
const urls = [`${api}/top/anime/1/tv`, `${api}/top/anime/1/airing`];
return Promise.all(
urls.map(async url => {
return await fetch(url); // fetch data from urls
})
)
.then((
responses // convert response to json and setState to retrieved data
) =>
Promise.all(responses.map(resp => resp.json())).then(data => {
// if you care about mutation use this
const topTvFiltered = data[0].top.filter( (item) => item.rank <= 6 );
const topAiringFiltered = data[1].top.filter( (item) => item.rank <= 6 );
this.setState({
topTv: topTvFiltered,
topAiring: topAiringFiltered
});
})
)
.catch(err => console.log("There was an error:" + err));
}
componentDidMount() {
this.getData();
}
render() {
const { topTv, topAiring } = this.state;
return (
<React.Fragment>
{ topTv.length > 0 ? <h2>Top TV</h2> : null }
{this.state.topTv.map((item, index) => (
<TopAnime key={index} title={item.title} image={item.image_url} />
))}
{ topAiring.length > 0 ? <h2>Top airing</h2> : null }
{this.state.topAiring.map((item, index) => (
<TopAnime key={index} title={item.title} image={item.image_url} />
))}
</React.Fragment>
);
}
}
I am breaking down a larger post into smaller questions. Please understand I never used Promise before and that I am new to React-Native too. It would be great to get feedback and recommendations on how to setup API calls and handle the data. Thank you in advance.
How can I dynamically create URLs for API requests? Here's what I am trying to achieve:
Pseudocode
Child
Retrieve two variables
Use these two variables to build an URL
Trigger the first Promise and resolve
Retrieve another two variables
Use these two variables to build a new an URL
Trigger the second Promise and resolve
Gather the data from both promises and pass to parent
Parent
Retrieve data from Child
Get data from the first Promise and set to a state
Get data from the second Promise and set to another state
APIservice.js
Child
class APIservice {
_getStopPoint = (endpoint) => {
return new Promise(function(resolve, reject) {
fetch(endpoint)
.then((response) => response.json())
.then((data) => {
console.log("APIservice StopPoint", data)
resolve(data);
});
});
};
};
module.exports = new APIservice
List.js
Parent
As you can see, the way I setup the endpoint is lame. It's not ideal as the URL is the same. I want to structure something that can receive two variables and build the URL on the go. Something like https://api.tfl.gov.uk/Line/${routeid}/Arrivals/${stationid}.
If I manage that, how can I pass the API call to the APIservice having only one endpoint that dynamically will change based on the two variables it receives? I am not sure how to differentiate the call in the Promise.all having only "one" URL.
let APIservice = require('./APIservice')
let endpoint = 'https://api.tfl.gov.uk/Line/55/Arrivals/490004936E'
let endpoint1 = 'https://api.tfl.gov.uk/Line/Northern/Arrivals/940GZZLUODS'
export class List extends Component {
constructor(props) {
super(props);
this.state = {
bus: null,
tube: null,
}
};
componentWillMount() {
let loadData = (endPoint) => {
Promise.all([
APIservice._getStopPoint(endpoint),
APIservice._getStopPoint(endpoint1),
])
.then((data) => {
// Name for better identification
const listBus = data[0]
const listTube = data[1]
this.setState({
bus: listBus,
tube: listTube
}, () => {
console.log("bus", this.state.bus, "tube", this.state.tube)
});
})
.catch((error) => {
console.log(error)
})
}
loadData(endpoint);
loadData(endpoint1);
}
render() {
return(
<View>
<FlatList
data={this.state.bus}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
<FlatList
data={this.state.tube}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
</ View>
);
}
};
It is pretty easy to implement what you are saying once you understand how this works.
You are using fetch for your API calls which returns a Promise upon use. The pseudo-code for your use case would be something like this:
class APIService {
static fetchFirst(cb) {
fetch('FIRST_URL')
.then(resp => {
try {
resp = JSON.parse(resp._bodyText);
cb(resp);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
static fetchSecond(routeid, stationid, cb) {
fetch(`https://api.tfl.gov.uk/Line/${routeid}/Arrivals/${stationid}`)
.then(resp => {
try {
resp = JSON.parse(resp._bodyText);
cb(resp);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
}
module.exports = APIService;
Include this in your parent component and use it as follows:
let APIService = require('./APIService')
export class List extends Component {
constructor(props) {
super(props);
this.state = {
bus: null,
tube: null,
}
};
componentWillMount() {
APIService.fetchFirst((resp1) => {
APIService.fetchSecond(resp1.routeid, resp1.stationid, (resp2) => {
this.setState({
tube: resp2
});
});
});
}
render() {
return(
<View>
<FlatList
data={this.state.bus}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
<FlatList
data={this.state.tube}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
</ View>
);
}
};
I haven't checked the errors on the callback function, please see that the errors are handled when you use this.