I have a script which calls API from React and then triggers email notification function.
I was changing one part of it to call whole array of parameters instead of calling one parameter after another.
Here is part before change(working one). Console log shows correct response and I receive email notification as well.
const getApiData = () => {
const apiCall = (symbol) => {
return `https://min-api.cryptocompare.com/data/pricemulti?fsyms=${symbol}&tsyms=USD&api_key=API-KEY-HERE`
}
const MAX_CHARACKTERS = 300
let bucketArray = ['']
for (let i=0; i < assets.length - 1; i += 1) {
const symbol = `${bucketArray[bucketArray.length - 1]},${assets[i]}`
if (i === 0) {
bucketArray[0] = assets[i]
continue
}
if (symbol.length < MAX_CHARACKTERS) {
bucketArray[bucketArray.length - 1] = symbol
} else {
bucketArray[bucketArray.length] = assets[i]
}
}
const getData = () => {
Promise.all(
bucketArray.map(req => {
return axios(apiCall(req))
.then(({ data }) => data)
})
).then((data) => setDataApi(data))
}
getData()
};
Here is problematic one.
const getApiData = () => {
const getString = symbol =>
`https://min-api.cryptocompare.com/data/pricemulti?fsyms=${symbol}&tsyms=USD&api_key=API-KEY-HERE`;
function getAxious(id) {
const url = getString(id);
return axios.get(url);
}
const BUCKET_SIZE = 150;
const bucketArray = assets.reduce(
(arr, rec) => {
if (arr[arr.length - 1].length < BUCKET_SIZE) {
arr[arr.length - 1] = [...arr[arr.length - 1], rec];
return arr;
}
return [...arr, [rec]];
},
[[]]
);
bucketArray
.reduce((acc, rec) => {
return acc.then(results => {
return Promise.all(
rec.map(item =>
getAxious(item).then(({ data }) => {
return {
Symbol: item,
Open: data
};
})
)
).then(x => {
return [...x, ...results];
});
});
},
Promise.resolve([]))
.then(res => {
setDataApi(res);
});
};
Here in console I receive empty array - [] no errors showed, but email notification also stops from working.
I'm changing the code since I need to call whole array from API in one call. Before I was calling one symbol after another.
What I did wrong that console doesn't show the correct response?
EDIT1
Here is bucketArray value
const assets = ['ADA','KAVA','DOGE'];
I was not able to understand completely, but I think you want to collect all the results together and set it to the data using setDataApi.
Check the below code and let me know if it helps:
async function getApiData() {
const getString = (arr) =>
`https://min-api.cryptocompare.com/data/pricemulti?fsyms=${arr.join(
","
)}&tsyms=USD&api_key=API_KEY`;
function getAxious(arr) {
const url = getString(arr);
return axios.get(url);
}
const BUCKET_SIZE = 150;
const bucketArray = assets.reduce(
(arr, rec) => {
if (arr[arr.length - 1].length < BUCKET_SIZE) {
arr[arr.length - 1] = [...arr[arr.length - 1], rec];
return arr;
}
return [...arr, [rec]];
},
[[]]
);
const res = await getAxious(bucketArray);
console.log("res", res);
return res;
// after this you can set setDataApi(res);
}
// keep this useEffect sepearate
const [timer, setTimer] = useState(null);
useEffect(() => {
async function getApiDatahandler() {
const res = await getApiData();
console.log(res);
const timerId = setTimeout(() => {
getApiDatahandler();
}, 1000 * 60);
setTimer(timerId);
setDataApi(res)
// set the data setDataApi(res);
}
getApiDatahandler();
return () => {
window.clearTimeout(timer);
};
}, []);
// useEffect(() => {
// const timerId = setTimeout(() => {
// getApiData();
// }, 1000 * 60);
// }, [])
Checkout this codepen for a possible solution.
https://codepen.io/bcaure/pen/RwapqZW?editors=1011
In short, I don't know how to fix your code because it's quite a callback hell.
// Mock API and data
const bucketArray = [[{item: 'item1'}], [{item: 'item2'}], [{item: 'item3'}]];
const getAxious = item => {
return new Promise((resolve, reject) => resolve({data: 'API data'}));
}
// Return promise that combines API data + input item
const recToPromise = rec => rec.map(item => {
return new Promise((resolve, reject) => getAxious(item)
.then(data => resolve({item, data})));
});
// Flatten array
const recPromisesFlatten = bucketArray.flatMap(recToPromise);
Promise.all(recPromisesFlatten)
.then(res => {
const flattenRes = res.flatMap(({item, data}) => ({ Symbol: item, Open: data }));
console.log(JSON.Stringify(flattenRes))
});
What I'm suggesting to debug errors:
build your promise array first
then run Promise.all
then combine your data
Bonus: you can see flatMap instead of reduce for better readability.
Related
I am trying to execute allCountryData and return a promise its working fine but after allCountryData is done executing I want to perform a operation on that returned data / or allCountryDataArray and store the highest values in arrayOfHighestCases
Note I can't chain the other login in allCountryData.
Please help let me know if you need any more details
export const allCountryDataArray = [];
export const arrayOfHighestCases = [];
const allCountryData = async () => {
sendHTTP()
.then((res) => {
return res.response;
})
.then((res) => {
allCountryDataArray.push(...res);
return allCountryDataArray;
});
return await allCountryDataArray;
// Highest Cases
};
The code is below is not working
const highestCasesData = async () => {
// const allCountryDataArrayy = await allCountryData();
// allCountryData()
// .then((data) => {
// console.log(arrayOfHighestCases[0]);
// })
// .then((res) => {
const np = new Promise((res, rej) => {
res(allCountryData());
});
return np.then((res) => {
console.log(res);
const arrayofHigh = allCountryDataArray.sort((a, b) => {
if (a.cases.total < b.cases.total) {
return 1;
} else if (a.cases.total > b.cases.total) {
return -1;
} else {
return 0;
}
});
console.log(arrayofHigh);
const slicedArray = arrayofHigh.slice(0, 6);
for (const eachHighCase of slicedArray) {
arrayOfHighestCases.push(eachHighCase);
}
console.log(arrayOfHighestCases);
return arrayOfHighestCases;
});
// });
};
highestCasesData();
Filling global arrays with async data is a way into timing conflicts. Bugs where the data ain't there, except when you look it is there and yet another question here on my SO about "Why can't my code access data? When I check in the console everything looks fine, but my code ain't working."
If you want to store something, store Promises of these arrays or memoize the functions.
const allCountryData = async () => {
const res = await sendHTTP();
return res.response;
};
const highestCasesData = async () => {
const allCountryDataArray = await allCountryData();
return allCountryDataArray
.slice() // make a copy, don't mutate the original array
.sort((a, b) => b.cases.total - a.cases.total) // sort it by total cases DESC
.slice(0, 6); // take the first 6 items with the highest total cases
}
This is working please let me know if I can make some more improvements
const allCountryData = async () => {
return sendHTTP()
.then((res) => {
return res.response;
})
.then((res) => {
allCountryDataArray.push(...res);
return allCountryDataArray;
});
// Highest Cases
};
const highestCasesData = async () => {
return allCountryData().then((res) => {
console.log(res);
const arrayofHigh = allCountryDataArray.sort((a, b) => {
if (a.cases.total < b.cases.total) {
return 1;
} else if (a.cases.total > b.cases.total) {
return -1;
} else {
return 0;
}
});
console.log(arrayofHigh);
const slicedArray = arrayofHigh.slice(0, 6);
for (const eachHighCase of slicedArray) {
arrayOfHighestCases.push(eachHighCase);
}
console.log(arrayOfHighestCases);
return arrayOfHighestCases;
});
};
highestCasesData();
Inside an async function i have a loop and inside this loop i need to use await to resolve a promise from another async function.
async function smallestCities(states) {
const citiesInState = [];
for (const state of states) {
const length = await lengthOfState(state.Sigla);
const stateObject = {
state: state.Sigla,
cities: length,
};
citiesInState.push(stateObject);
}
citiesInState.sort((a, b) => {
if (a.cities > b.cities) return 1;
if (a.cities < b.cities) return -1;
return 0;
});
return citiesInState.filter((_, index) => index < 5).reverse();
}
It's work fine, but eslint says to disallow await inside of loops and use Promise.all() to resolve all promises.
The problem is that my promises are in an object property:
How can i figure out to use Promise.all() with properties of an object?
Chain a .then onto the lengthOfState call to make the whole Promise resolve to the object you need, inside the Promise.all:
const citiesInState = await Promise.all(
states.map(
state => lengthOfState(state.Sigla).then(cities => ({ state: state.Sigla, cities }))
)
);
const NEW_LAND = 'newLand'
const ACTUAL_FINLAND = 'actualFinland'
const PIRKKAS_LAND = 'pirkkasLand'
const STATE_CITY_MAP = {
[NEW_LAND]: ['HELSINKI', 'VANTAA', 'KORSO'],
[ACTUAL_FINLAND]: ['TURKU'],
[PIRKKAS_LAND]: ['WHITE RAPIDS', 'NOKIA'],
}
const mockGetCities = (stateName) => new Promise((res) => {
setTimeout(() => { res([stateName, STATE_CITY_MAP[stateName]]) }, 0)
})
const compareStatesByCityQty = (a, b) => {
if (a[1].length > b[1].length) return 1
if (a[1].length < b[1].length) return -1
return 0
}
const getSmallestStates = async (stateNames, cityQty) => {
const cities = await Promise.all(stateNames.map(mockGetCities))
return cities
.sort(compareStatesByCityQty)
.reverse()
.slice(0, cityQty)
}
;(async () => {
const stateNames = [NEW_LAND, ACTUAL_FINLAND, PIRKKAS_LAND]
const smallestStates = await getSmallestStates(stateNames, 2)
console.log(smallestStates)
})()
Currently each of the value is set after setting the previous value, the async calls are not executed in parrallel. How do I make these calls execute in parallel?
const [index, setIndex] = useState(0);
const [roll, setRollNo] = useState(1);
const [sem, setSemester] = useState(1);
useEffect(() => {
getMyValue();
}, []);
const getMyValue = async () => {
try {
setIndex(JSON.parse(await AsyncStorage.getItem('#branch')) || 0);
setSemester(JSON.parse(await AsyncStorage.getItem('#sem')) || 1);
setRollNo(JSON.parse(await AsyncStorage.getItem('#roll')) || 1);
} catch (e) {
// console.log(e);
}
};
You can use Promise.all
const [index, semester, roll] = await Promise.all([
AsyncStorage.getItem('#branch'),
AsyncStorage.getItem('#sem'),
AsyncStorage.getItem('#roll')]);
setIndex(JSON.parse(index) || 0);
setSemester(JSON.parse(semester) || 1);
setRollNo(JSON.parse(roll) || 1);
Or if you like to turn such thing into mapping monstrosity as recommended in the answers there you go...
const params = ['#branch', '#sem', '#roll'];
const defaultValues = [0, 1, 1];
const [index, semester, roll] = await Promise.all(
params.map(AsyncStorage.getItem))
.then((values) => values.map((pr, index) => JSON.parse(pr) || defaultValues[index]));
setIndex(index);
setSemester(semester);
setRollNo(roll);
To execute several promises in parallel you need organize them as array and execute unsing Promise.all:
const [index, setIndex] = useState(0);
const [roll, setRollNo] = useState(1);
const [sem, setSemester] = useState(1);
useEffect(() => {
getMyValue();
}, []);
const getMyValue = async () => {
try {
const itemsArr = ['#branch', '#sem', '#roll']
const result = await Promise.all(promisesArr.map(item => AsyncStorage.getItem(item)))
setIndex(JSON.parse(result[0]) || 0);
setSemester(JSON.parse(result[1]) || 1);
setRollNo(JSON.parse(result[2]) || 1);
} catch (e) {
// console.log(e);
}
};
React JS does not batch the state updates if the event handler is async.
In your example, as you are await-ing on AsyncStorage.getItem, they are not batched.
You can #Józef 's solution to batch them up
Reference: https://github.com/facebook/react/issues/14259#issuecomment-450118131
Using for await of ...
We use an array of jobs to getItem, then after await, we can set the state of each item based on its type.
#Jozef's answer is also a great option as well, very good use of array deconstruction there
const response = []
const jobs = ["#branch", "#sem", "#roll"]
for await (item of jobs) {
const res = await AsyncStorage.getItem(item);
response.push({
type: item,
res
})
}
/**
response
[
{ type: "#branch", res: <res> },
{ type: "#sem", res: <res> },
{ type: "#roll", res: <res> }]
*/
response.forEach(item => {
const { type, res } = item;
if (type === "#branch") {
setIndex(JSON.parse(res))
} else if (type === "#semester") {
setSemester(JSON.parse(res))
} else if (type === "#roll") {
setRollNo(JSON.parse(res))
}
})
you don't need a async await here as you don't want to wait just use a promise instead
const getMyValue2 = () => {
try {
Promise.all([
Promise.resolve( AsyncStorage.getItem('#branch')),
Promise.resolve( AsyncStorage.getItem('#sem')),
Promise.resolve( AsyncStorage.getItem('#roll'))
]).then(data => {
setIndex(JSON.parse(data[0] || 0));
setRollNo(JSON.parse(data[1] || 1));
setSemester(JSON.parse(data[2] || 1));
});
} catch (e) {
// console.log(e);
}
};
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)