Not sure how to go about this.
I fetch from 1 URL to get an array of objects like this:
[{name: 'apple', url: 'appleURL'}, {name: 'orange', 'orangeURL', ...}]
then map through each URL to fetch data, the returned data for each item has another URL... like this:
[{name: 'apple', colors: ['green', 'red'], type: 'http://URL-that-needs-fetching'},{...etc}]
heres what i have, and it works but it's extremely slow...
useEffect(() => {
const fetchFruit = async () => {
setLoading(true);
const fruitResponse = await fetch(fruitPath);
const fruitJson: Fruit =
await fruitResponse.json();
const fruitDetails = await Promise.all(
fruitJson.results.map(async (f: FruitURLs) => {
const fDetails = await fetch(f.url);
const json: FruitDetails = await fDetails.json();
return json;
})
);
const getFruitDescriptions = await Promise.all(
fruitDetails.map(async (item: FruitDetails, index: number) => {
const fruitType = await fetch(item.type.url);
const json: FruitInfo = await fruitType.json();
return json;
})
);
let full: FruitDetails[] = fruitDetails;
for (let index = 0; index < fruitDetails.length; index++) {
full[index].fruitInfo = getFruitDescriptions[index];
}
setFruitDetails(full);
setLoading(false);
};
fetchFruit();
}, []);
I'm new to using promises and not 100% confident with them, also new to TS, and react-native so apologies if there's mistakes here....
The problem is that by using await inside Promise.all(), you are resolving the fetches instead of having them happen in parallel and returning it wrapped by a promise.
You can take a reference:
const fetchFruit = async () => {
const fruitResponse = await fetch(fruitPath);
const fruitJson: Fruit = await fruitResponse.json();
const fruitDetails = await Promise.all(
fruitJson.results.map(async (f: FruitURLs) => fetch(f.url))
);
const getFruitDescriptions = await Promise.all(
fruitDetails.map(async (item: FruitDetails, index: number) =>
fetch(item.type.url)
)
);
const [fruitDetailsAsJson, getFruitDescriptionsAsJson] = Promise.all([
fruitDetails.map(async (f) => f.json()),
getFruitDescriptions.map(async (f) => f.json()),
]);
const full = fruitDetailsAsJson.map((fruit, index) => ({
...fruit,
fruitInfo: getFruitDescriptionsAsJson[index],
}));
setFruitDetails(full);
setLoading(false);
};
Use this resource to learn about the best practice.
If there are a lot of entries in the fruitresponse, and the back end cannot aggregate all the data, then it is inevitable to be slow. At this time, we should consider loading a little and displaying a little
const FruitList = ()=> {
const { data, loading } = useFetch(fruitPath);
if (loading) return <div>Loading...</div>
return data.results.map(x=> <Fruit key={x.id} data={x} />)
}
const Fruit = ({data}: { data: FruitDetails })=> {
const { data, loading } = useFetch(data.url);
if (loading) return <div>Loading...</div>;
return (
<div>
{data.name}
<FruitDescription url={data.type.url} />
</div>
)
}
const FruitDescription = ({url}: { url: string })=> {
const { data, loading } = useFetch(url);
if (loading) return <div>Loading...</div>;
return <div>{data.description}</div>
}
Related
I would like to be able to sort my array so I don't have duplicates into my "Recently-Viewed" section. The recently viewed section works fine except that it breaks when I add a duplicate. So I want to be able to sort my array so it doesn't break. I'm not really sure how to implement a sort function. Do I use filter or what do I do? I'm really confused.
My code:
const [tvShow, setTVShow] = useState([]);
const [recentlyViewed, setRecentlyViewed] = useState([]);
const getMovieRequest = async () => {
const url = `https://api.themoviedb.org/3/movie/top_rated?api_key=1e08baad3bc3eca3efdd54a0c80111b9&language=en-US&page=1`;
const response = await fetch(url);
const responseJson = await response.json();
setTVShow(responseJson.results)
};
useEffect(() => {
getMovieRequest();
},[]);
useEffect(() => {
const recentlyMovies = JSON.parse(localStorage.getItem('react-movie-app-favourites')
);
if (recentlyMovies) {
setRecentlyViewed(recentlyMovies.slice(0,5));
}
}, []);
const saveToLocalStorage = (items) => {
localStorage.setItem('react-movie-app-favourites', JSON.stringify(items))
};
const addRecentlyViewed = (movie) => {
const newRecentlyViewed = [movie, ...recentlyViewed]
setRecentlyViewed(newRecentlyViewed.slice(0,5));
saveToLocalStorage(newRecentlyViewed);
if (newRecentlyViewed > 5) {
newRecentlyViewed.pop();
}
};
Thank you guys in advance. I'm new to React and I find this very confusing.
Using the Set constructor and the spread syntax:
uniq = [...new Set(array)];
useEffect(() => {
const recentlyMovies = [...new Set(JSON.parse(localStorage.getItem('react-movie-app-favourites')))];
if (recentlyMovies) {
setRecentlyViewed(recentlyMovies.slice(0,5));
}
}, []);
I am building an app with react/ redux for managing Collection of Electronic equipment (=donations).
In the first stage I need to make 2 api calls:
the first and the second are donations data and donors data (kept as different collections in mongodb) and then combine them. This info is shown in a donation route.
The action looks like this:
const basicUrl = 'http://localhost:8000/api';
export const requestDonor_DonationData = () => getDonationData (
`${basicUrl}/donor`,
`${basicUrl}/donation`
);
and the getDonationData func looks like this:
import {
REQUEST_ENTITIES_PENDING,
REQUEST_ENTITIES_SUCCES,
REQUEST_ENTITIES_FAILED
} from './constants';
export const getDonationData = (urlDonor, urlDonation) => (dispatch) => {
dispatch ( {type: REQUEST_ENTITIES_PENDING} );
Promise.all([
fetch(urlDonor).then(res => res.json()),
fetch(urlDonation).then(res => res.json())
]).then ( ([ donorResult, donationResult]) => donorResult.data.map( (e, i) => Object.assign(e, donationResult.data[i]) ) )
.then( mergedData => dispatch({type: REQUEST_ENTITIES_SUCCES, payload: mergedData }) )
.catch(error => dispatch({type: REQUEST_ENTITIES_FAILED, payload: error}) )
}
that works fine.
In the second stage, When a donation have been peeked up, it become an equipment (not the perfect word..) which means that now it is waiting for inspection. this info is shown in a equipment route.
the equipment data contain the donationId and status (different from the donation status).
Now I want to do something similar:
make 3 api calls (getting donor, donation, & equipment data)
merging the donor whit its donation data
filtering the merged
data with the donations that have been peeked up (status='DONE')
create a new json which takes the merged data and replace the ID and
status of donation with the ID and status of the equipment.
I tried
to do that with the first approach (just with Promise.all) but found
it very confusing working with multiple ".then" ...
this is what I tried :
the action-
export const requestEquipmentData = () => getEquipmentData (
[
`${basicUrl}/donor`,
`${basicUrl}/donation`,
`${basicUrl}/equipment`
]
);
export const getEquipmentData = (urls) => (dispatch) => {
dispatch ( {type: REQUEST_ENTITIES_PENDING} );
try {
const [ donorResult, donationResult, equipmentResult ] = Promise.all(urls.map(async function(url) {
const response = await fetch(url);
return response.json();
}));
const donationInfo = donorResult.data.map( (e, i) => Object.assign(e, donationResult.data[i]) );
const filteredDonation = donationInfo.filter(item =>item.status==='DONE');
const equipment = filteredDonation.map( (donation,i) => {
let obj = donation;
obj.id = equipmentResult.data[i].id;
obj.status = equipmentResult.data[i].status;
return obj;
})
dispatch({type: REQUEST_ENTITIES_SUCCES, payload: equipment });
} catch (error) {
dispatch({type: REQUEST_ENTITIES_FAILED, payload: error})
}
}
but I am doing somethig wrong, and that is the error:
type: "REQUEST_ENTITIES_FAILED", payload: TypeError: undefined is not a function
I would appreciate any help
The result of Promise.all() is a Promise that resolves to the array of results. It is not an array itself so you cannot destructure it like this.
You can use the same .then() approach that you used in your first example:
export const getEquipmentData = (urls) => (dispatch) => {
dispatch({ type: REQUEST_ENTITIES_PENDING });
Promise.all(urls.map(async function (url) {
const response = await fetch(url);
return response.json();
})).then(([donorResult, donationResult, equipmentResult]) => {
const donationInfo = donorResult.data.map((e, i) => Object.assign(e, donationResult.data[i]));
const filteredDonation = donationInfo.filter(item => item.status === 'DONE');
const equipment = filteredDonation.map((donation, i) => {
let obj = donation;
obj.id = equipmentResult.data[i].id;
obj.status = equipmentResult.data[i].status;
return obj;
})
dispatch({ type: REQUEST_ENTITIES_SUCCES, payload: equipment });
}).catch(error) {
dispatch({ type: REQUEST_ENTITIES_FAILED, payload: error })
}
}
Or you can use async/await syntax. Checkout this question for a generally discussion on resolving an array of Promises.
export const getEquipmentData = (urls) => async (dispatch) => {
dispatch ( {type: REQUEST_ENTITIES_PENDING} );
try {
const [ donorResult, donationResult, equipmentResult ] = await Promise.all(urls.map(async function(url) {
const response = await fetch(url);
return response.json();
}));
const donationInfo = donorResult.data.map( (e, i) => Object.assign(e, donationResult.data[i]) );
const filteredDonation = donationInfo.filter(item =>item.status==='DONE');
const equipment = filteredDonation.map( (donation,i) => {
let obj = donation;
obj.id = equipmentResult.data[i].id;
obj.status = equipmentResult.data[i].status;
return obj;
})
dispatch({type: REQUEST_ENTITIES_SUCCES, payload: equipment });
} catch (error) {
dispatch({type: REQUEST_ENTITIES_FAILED, payload: error})
}
}
In my opinion your general approach here is not good. You should read the guides on Normalizing State Shape. It seems like your APIs are returning normalized data and then your are "unnormalizing" it by combining data from multiple endpoints.
I'm trying to get podcast data with `SpotifyAPI. I am fetching the data on my node.js server and sending it as a json to client. I receive a typed object and trying to push this object to an array as I want to have an array of 50 podcasts each of them being an object:
export interface Episode {
name: string;
description: string;
date: string;
img: string;
url: string
};
I'm sending token (to whole function component) and ids of shows I want to fetch
const fetchEpisodesData = async (ids: string[]) => {
let arr: any[] = [];
ids.forEach(async (id, index) => {
const response = await fetch(
`http://localhost:8080/podcastEpisodes?access_token=${tokenCode}&id=${id}`
)
const dataResponse = await response.json();
arr.push(dataResponse)
// )
});
console.log(arr);
};
When I console.log(arr) it shows what I pretty much want and an array of objects but when I console.log(arr[1]) it returns undefined.
Any ideas?
Here is my code of the fetchinng function
export const useFetch = (tokenCode: string) => {
// console.log(tokenCode);
let array: any[] = [];
const [data, setData]: any = useState([]);
const [loading, setLoading] = useState(true);
const getData = async (id: string) => {
const response = await fetch(
`http://localhost:8080/podcastEpisodes?access_token=${tokenCode}&id=${id}`
);
const dataResponse = await response.json();
return dataResponse;
};
const fetchdata = async () => {
const response = await fetch(
`http://localhost:8080/podcast?access_token=${tokenCode}`
);
const dataResponse = await response.json();
// const item = dataResponse.results[0];
const myData = Object.keys(dataResponse).map((key) => {
return dataResponse[key];
});
// console.log(myData[0].show.id)
const idData = myData.map((data) => {
return data.show.id as string;
});
// console.log(idData, 'idData')
return idData;
// console.log(dataResponse);
};
const fetchEpisodesData = async (ids: string[]) => {
let arr: any[] = [];
ids.forEach(async (id, index) => {
const response = await fetch(
`http://localhost:8080/podcastEpisodes?access_token=${tokenCode}&id=${id}`
)
const dataResponse = await response.json();
arr.push(dataResponse)
// )
});
console.log(arr);
};
useEffect(() => {
fetchdata().then((res) => {
// console.log(res);
fetchEpisodesData(res);
});
}, [tokenCode]);
return { data, loading };
};
The issue here is a bit subtle.
You are pushing only one element, the dataResponse object, to your array arr here:
arr.push(dataResponse)
Since you don't push any other value, and arrays in Javascript are 0-indexed, you will only find your value in arr[0], but arr[1] will return undefined.
Now, if you wanted to fill your array arr with an array that would be getting in dataResponse, then you should spread the array when doing .push(), so you'll be pushing all elements found in such dataResponse array object.
Like so:
const dataResponse = await response.json();
// this works only **if** dataResponse is an array
// and you want to add all elements to your 'arr' array
arr.push(...dataResponse);
I am having some troubles getting several functions loading in the correct order. From my code below, the first and second functions are to get the companyID companyReference and are not reliant on one and another.
The third function requires the state set by the first and second functions in order to perform the objective of getting the companyName.
async componentDidMount() {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
a;
b;
c;
}
componentWillUnmount() {
this.isCancelled = true;
}
companyIdParams = () => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
});
};
getCompanyReference = () => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
});
});
};
getCompanyName = () => {
const { firebase } = this.props;
const { companyID, companyReference } = this.state;
const cid = companyID;
if (companyReference.includes(cid)) {
const getCompany = firebase.company(cid);
getCompany.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyName: doc.data().companyName,
loading: false
});
});
} else if (cid !== null && !companyReference.includes(cid)) {
navigate(ROUTES.INDEX);
}
};
How can I achieve this inside componentDidMount?
setState is asynchronous, so you can't determinate when the state is updated in a sync way.
1)
I recommend you don't use componentDidMount with async, because this method belongs to react lifecycle.
Instead you could do:
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
}
2)
The companyIdParams method doesn't have a return, so you are waiting for nothing.
If you need to wait I would return a promise when setState is finished;
companyIdParams = () => {
return new Promise(resolve => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
}, () => { resolve() });
});
};
The same for getCompanyReference:
getCompanyReference = () => {
return new Promise(resolve => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
}, () => { resolve() });
});
});
};
3)
If you want to parallelize the promises, you could change the previous code to this:
const [a, b] = await Promise.all([
await this.companyIdParams(),
await this.getCompanyReference()
]);
4)
According to your code, the third promise is not a promise, so you could update (again ;) the above code:
const [a, b] = .....
const c = this.getCompanyName()
EDIT: the bullet points aren't steps to follow
As the last api call is dependent on the response from the first 2 api calls, use a combination of Promise.all which when resolved will have the data to make the last dependent call
async componentDidMount() {
let [a, c] = await Promise.all([
this.companyIdParams(),
this.getCompanyReference()
]);
const c = await this.getCompanyName();
}
As I run this code, I wished to add the array final to the firestore -> sendGrid collection but it's always empty, although when I print it, it actually has the values.
I believe this is because of the timing issue, I always get [] -> value is just evaluated now (warning) and when I expand it it has the value.
function test() {
let today = new Date();
let addedDate = new Date(today.addDays(7));
let final = [];
let counter = 0;
let adder = new Promise(function (resolve, reject) {
db.collection("email").get()
.then((querySnapshot) => {
console.log(querySnapshot);
if (querySnapshot.empty !== true) {
querySnapshot.forEach((data) => {
console.log(data.data());
console.log(data.id);
let db2 = db.collection("email").doc(data.id);
let foodArr = [];
if (data.data() !== null) {
console.log(addedDate);
if (addedDate >= userList[0].exxpiaryDate) {
console.log("True");
}
db2.collection("list").where("expiaryDate", "<", addedDate.getTime()).get()
.then((list) => {
if (list.empty !== true) {
list.forEach((food) => {
if (food !== null) {
let temp = {
name: food.data().name,
time: food.data().expiaryDate,
};
foodArr.push(temp);
console.log(foodArr);
}
})
}
if (foodArr.length !== 0) {
let emailArr = {
email: data.data().email,
food: foodArr
};
console.log(emailArr);
final[counter] = (emailArr);
counter++;
console.log(final[0]);
}
}).catch((err) => {
console.log(err);
});
}
});
}
console.log(final);
resolve(final);
}).catch((err) => {
console.log(err);
});
});
return adder;
}
async function add() {
let add = await test();
console.log(add);
db.collection("sendGrid").add({
response: add
}).then((item) => {
console.log(item);
}).catch((err) => {
console.log(err);
});
}
Some points: (google if unsure why)
- prefer const
- return early
- clean code (eg. from console.log:s)
- cache fn calls
- functional programming is neat, look up Array.(map, filter, reduce, ...)
- destructuring is neat
- use arr[arr.length] = x; or arr.push(x), no need to manage your own counter
- short-circuit is sometimes neat (condition && expression; instead of if (condition) expression;)
- is queryResult.empty a thing? If it's a normal array, use !arr.length
- define variables in the inner most possible scope it's used in
- if having a promise in an async, make sure to return it
- prefer arrow functions
I changed the code to follow those points:
const test = ()=> {
const today = new Date();
const addedDate = new Date(today.addDays(7));
return new Promise((resolve)=> {
const final = [];
const emailsQuery = db.collection("email")
// an async/promise/then that's inside another promise, but not returned/awaited
emailsQuery.get().then((querySnapshot) => {
querySnapshot
.map(data=> ({id: data.id, data: data.data()}))
.filter(o=> o.data)
.forEach(({id, data: {email}}) => {
const db2 = db.collection("email").doc(id);
const itemsQuery = db2.collection("list").where("expiaryDate", "<", addedDate.getTime())
// another one!
itemsQuery.get().then((items) => {
const food = items.filter(o=> o).map(o=> o.data()).filter(o=> o)
.forEach(({name, expiaryDate: time})=> ({name, time}))
food.length && final.push({email, food})
}).catch(console.error);
});
// resolving before the two async ones have finished!!
resolve(final);
}).catch(console.error);
});
}
const add = async ()=> {
let response = await test();
return db.collection("sendGrid").add({response})
.then((item) => console.log('item:', item))
.catch(console.error)
}
Now, we can see that there is an issue with the async flow ("timing issue" in your words). I'll add one more best practice:
- use async/await when possible
Changing using that one makes it more clear, and solves the issue:
const test = async ()=> {
const today = new Date();
const addedDate = new Date(today.addDays(7));
const emailsQuery = db.collection("email")
const querySnapshot = await emailsQuery.get()
const emailEntries = querySnapshot
.map(data=> ({id: data.id, data: data.data()}))
.filter(o=> o.data)
// invoking an async fn -> promise; map returns the result of all invoked fns -> array of promises
const promisedItems = emailEntries.map(async ({id, data: {email}}) => {
const db2 = db.collection("email").doc(id);
const itemsQuery = db2.collection("list").where("expiaryDate", "<", addedDate.getTime())
const items = await itemsQuery.get()
const food = items.filter(o=> o).map(o=> o.data()).filter(o=> o)
.forEach(({name, expiaryDate: time})=> ({name, time}))
return {email, food}
});
const items = await Promise.all(promisedItems)
return items.filter(item=> item.food.length)
}
const add = async ()=> {
let response = await test();
return db.collection("sendGrid").add({response})
.then((item) => console.log('item:', item))
.catch(console.error)
}
Now, the flow is clear!
.
Even more concise (though lack of var names -> less clear) - just for kicks:
// (spelled-fixed expiryDate); down to 23% loc, 42% char
const getUnexpiredFoodPerEmails = async ({expiryDateMax} = {
expiryDateMax: new Date(new Date().addDays(7)),
})=> (await Promise.all((await db.collection('email').get())
.map(data=> ({id: data.id, data: data.data()})).filter(o=> o.data)
.map(async ({id, data: {email}})=> ({
email,
food: (await db.collection('email').doc(id).collection('list')
.where('expiryDate', '<', expiryDateMax.getTime()).get())
.filter(o=> o).map(o=> o.data()).filter(o=> o)
.forEach(({name, expiryDate: time})=> ({name, time})),
}))
)).filter(item=> item.food.length)
const add = async ()=> db.collection('sendGrid').add({
response: await getUnexpiredFoodPerEmails(),
}).then(console.log).catch(console.error)
// ...or with names
const getUnexpiredFoodListForEmailId = async ({id, expiryDateMax} = {
expiryDateMax: new Date(new Date().addDays(7)),
})=> (await db.collection('email').doc(id).collection('list')
.where('expiryDate', '<', expiryDateMax.getTime()).get())
.filter(o=> o).map(o=> o.data()).filter(o=> o)
.forEach(({name, expiryDate})=> ({name, time: expiryDate}))
const getEmails = async ()=> (await db.collection('email').get())
.map(data=> ({id: data.id, data: data.data()})).filter(o=> o.data)
const getUnexpiredFoodPerEmails = async ({expiryDateMax} = {
})=> (await Promise.all((await getEmails()).map(async ({id, data})=> ({
email: data.email,
food: await getUnexpiredFoodListForEmailId({id, expiryDateMax}),
})))).filter(item=> item.food.length)
const add = async ()=> db.collection('sendGrid').add({
response: await getUnexpiredFoodPerEmails(),
}).then(console.log).catch(console.error)