React Native - UseEffect Constantly Updates - javascript

I am using useEffect to get the total jigsaw pieces from my async storage to be displayed on the home page. I use async storage to get the number of jigsaws stored in the storage then for each jigsaw, i add the total amount of jigsaw pieces. I do this within useEffect as the total amount of jigsaw pieces may change depending on if the user adds more jigsaw to their collection. However, when i use useEffect, my total amount of jigsaw pieces constantly updates and never stops.
Code:
let [totalPieces, setTotalPieces] = useState(0);
const [numJigsaw, setNumJigsaw] = useState([]);
const getNumOfJigsaw = async () => {
try {
setNumJigsaw(await AsyncStorage.getAllKeys());
} catch (e) {}
return numJigsaw;
};
const getAllJigsaw = async () => {
let jigsaws = await getNumOfJigsaw();
for (let z = 0; z < jigsaws.length; z++) {
let currentJigsaw = JSON.parse(await AsyncStorage.getItem(jigsaws[z]));
setTotalPieces(totalPieces+ parseFloat(currentJigsaw.pieces));
}
};
useEffect(() => {
getAllJigsaw();
}, [totalPieces]);
I believe the issue is because totalPieces is dependant? Im not entirely sure though.

Yes - you've set the effect to run when totalPieces changes (via the dependency).
The effect (when the async stuff resolves) sets totalPieces, so the effect is run again due to that dependency, etc, etc.
It sounds like you're looking for something like
const [totalPieces, setTotalPieces] = useState(0);
const [jigsawKeys, setJigsawKeys] = useState([]);
useEffect(() => {
(async () => {
const jigsaws = await AsyncStorage.getAllKeys();
let total = 0;
for (let z = 0; z < jigsaws.length; z++) {
let currentJigsaw = JSON.parse(await AsyncStorage.getItem(jigsaws[z]));
total += parseFloat(currentJigsaw.pieces);
}
setJigsawKeys(jigsaws);
setTotalPieces(total);
})();
}, [totalPieces]);
all in all (I renamed numJigsaw to jigsawKeys so it makes more sense).

Related

function called from useEffect rendering too fast and breaks, works correctly after re-render

I am using React-id-swiper to load images for products. Let's say I have 5 products.
I am trying to use a function to detect when every image is successfully loaded, then when they are, set the useState 'loading' to false, which will then display the images.
I am using a variable 'counter' to count how many images there are to load. Once 'counter' is more than or equal to the amount in the list (5), set 'loading' to FALSE and load the images.
However, I am noticing that when I console log 'counter' when running it from useEffect, it ends at '0' and renders 5 times. 'Loading' also remains true.
However, if I trigger a re-render, I see 1,2,3,4,5 in the console. 'Loading' then turns to false.
I suspect this is to do with useEffect being too fast, but I can't seem to figure it out. Can somebody help?
Code:
const { products } = useContext(WPContext);
const [productsList, setProductsList] = useState(products);
const [filteredList, setFilteredList] = useState(products);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadImages(products);
}, []);
const loadImages = (allProducts) => {
let counter = 0;
setLoading(true);
const newProducts = allProducts.map((product) => {
const newImg = new Image();
newImg.src = "https://via.placeholder.com/450x630";
newImg.onload = function () {
counter += 1;
};
// At this point, if I console log 'counter', it returns as 0. If I trigger a re-render, it returns as 1,2,3,4,5
return {
...product,
acf: {
...product.acf,
product_image: {
...product.acf.product_image,
url: newImg.src,
},
},
};
});
handleLoading(newProducts, counter);
};
const handleLoading = (newProducts, counter) => {
if (counter >= filteredList.length) {
setLoading(false);
counter = 0;
setFilteredList(newProducts);
}
};
First weird thing is that you are calling the handleLoading() function before its even defined. This is probably not a problem but a weird practice.
You are also creating a new variable counter when you pass it in as an argument. You aren't actually reseting the counter that you want.
Also using onload is unnecessary and can cause weird behavior here since its not a synchronous operation but event based. As long as you set the img.src it should force it to load.
Try this:
const loadImages = (allProducts) => {
let counter = 0;
setLoading(true);
const newProducts = allProducts.map((product) => {
const newImg = new Image();
newImg.src = "https://via.placeholder.com/450x630";
counter += 1;
return { ...product, acf: { ...product.acf, product_image: { ...product.acf.product_image, url: newImg.src }}};
});
const handleLoading = (newProducts) => {
if (counter >= filteredList.length) {
setLoading(false);
counter = 0;
setFilteredList(newProducts);
}
};
handleLoading(newProducts);
};
useEffect(() => {
loadImages(products);
}, []);

For loop not working in React when using setState (DOM not showing up updated data)

What's up friends,
I have a problem. I have DOM elements that show data from the variable (useState) that I am saving with some "accounts".
I have a function that updates the data of each element by calling the api, and I have a function that refreshes all the elements one by one, the problem is that when using the function that refreshes all the elements, the changes are not reflected correctly in the dom but when using the function individually (I have a button for that) I can see the changes correctly reflected.
I do not get the error, if you can help me I would be very grateful.
Refresh Function
let [accounts, setAccounts] = useState
const refreshAccount = async (id, name, token) => {
await setIsBusy(true)
await updateWorkingStatus(id, true, name, token)
let work = await axios.get(
'http://192.168.0.101:3000/account/refresh/' + id,
)
await updateWorkingStatus(id, false, name, token)
await setIsBusy(false)
// console.log(work.data.result)
let accs = [...accounts]
let index = accs.findIndex((acc) => acc._id === id)
accs[index] = work.data.result
accs[index]._id = id
// console.log(accs[index])
setTimeout(() => {
setAccounts(accs)
}, 0)
return 'OK'
}
Refresh Selected Item (I already have a function that selects each item and works nice)
const refreshSelected = async () => {
let finalAccounts = await accounts.filter((acc) => acc.selected)
for (let i = 0; i < finalAccounts.length; i++) {
await refreshAccount(
finalAccounts[i]._id,
finalAccounts[i].name,
finalAccounts[i].token,
)
}
}
I know the problem is updating the status, but I can't solve it. Implement a setTimeout and the previous element to the last is updated or the last of the cycle, the previous ones are left with the outdated data (first state).
I think problem is when using FOR loop on React, but dont know how to use it correctly
The problem is probably that the setAccounts function is asynchronous, but the execution doesn't wait for it to complete. Because setAccounts is asynchronous, subsequent calls in the same update cycle will overwrite previous updates, and the previous changes will be lost, causing only the last update to take effect.
You could store the changes to an array and update it after all the API requests have finished.
let [accounts, setAccounts] = useState
const refreshAccount = async (id, name, token) => {
await setIsBusy(true)
await updateWorkingStatus(id, true, name, token)
let work = await axios.get(
'http://192.168.0.101:3000/account/refresh/' + id,
)
await updateWorkingStatus(id, false, name, token)
await setIsBusy(false)
// console.log(work.data.result)
let accs = [...accounts]
let index = accs.findIndex((acc) => acc._id === id)
accs[index] = work.data.result
accs[index]._id = id
return accs
}
const refreshSelected = async () => {
let finalAccounts = await accounts.filter((acc) => acc.selected)
let newAccounts = []
for (let i = 0; i < finalAccounts.length; i++) {
const accs = await refreshAccount(
finalAccounts[i]._id,
finalAccounts[i].name,
finalAccounts[i].token,
)
newAccounts = newAccounts.concat(accs)
}
setAccounts(newAccounts)
}

async functions not executing in the correct order inside a map function

I have created an async function that will extra the data from the argument, create a Postgres query based on a data, then did some processing using the retrieved query data. Yet, when I call this function inside a map function, it seemed like it has looped through all the element to extra the data from the argument first before it proceed to the second and the third part, which lead to wrong computation on the second element and onwards(the first element is always correct). I am new to async function, can someone please take at the below code? Thanks!
async function testWeightedScore(test, examData) {
var grade = [];
const testID = examData[test.name];
console.log(testID);
var res = await DefaultPostgresPool().query(
//postgres query based on the score constant
);
var result = res.rows;
for (var i = 0; i < result.length; i++) {
const score = result[i].score;
var weightScore = score * 20;
//more computation
const mid = { "testID": testID, "score": weightScore, more values...};
grade.push(mid);
}
return grade;
}
(async () => {
const examSession = [{"name": "Sally"},{"name": "Bob"},{"name": "Steph"}]
const examData = {
"Sally": 384258,
"Bob": 718239,
"Steph": 349285,
};
var test = [];
examSession.map(async sesion => {
var result = await testWeightedScore(sesion,examData);
let counts = result.reduce((prev, curr) => {
let count = prev.get(curr.testID) || 0;
prev.set(curr.testID, curr.score + count);
return prev;
}, new Map());
let reducedObjArr = [...counts].map(([testID, score]) => {
return {testID, score}
})
console.info(reducedObjArr);
}
);
})();
// The console log printed out all the tokenID first(loop through all the element in examSession ), before it printed out reducedObjArr for each element
The async/await behaviour is that the code pause at await, and do something else (async) until the result of await is provided.
So your code will launch a testWeightedScore, leave at the postgresql query (second await) and in the meantime go to the other entries in your map, log the id, then leave again at the query level.
I didn't read your function in detail however so I am unsure if your function is properly isolated or the order and completion of each call is important.
If you want each test to be fully done one after the other and not in 'parallel', you should do a for loop instead of a map.

Javascript looping through array with a promises function returning values in random order

My code is intended to loop through an array of sports team IDs to use them to compile various information from an API then add HTML to my page to display the most recently finished game for each team. It is working and looking great EXCEPT: the results are appearing in on my page in a random order each time I refresh. I'm thinking this has something to do with the loop moving to the next iteration quickly, while the responses from the server are coming back at random times. I expect the result to be "Patriots" first, then "Steelers" etc but the result is random, with Patriots hardly ever coming up first.
Also, I'm very new to JS, so I'm sure there is a ton I could do to make this better, so thank you in advance!
//Define the teams I want scores from and their API reference numbers.
let teams = new Map();
teams.set("Patriots", 134920);
teams.set("Steelers", 134925);
teams.set("Bruins", 134830);
teams.set("Penguins", 134844);
teams.set("Celtics", 134860);
teams.set("Red Sox", 135252);
teams.set("Huskers", 136923);
let teamArr = Array.from(teams.values());
for (i = 0; i < teamArr.length; i++) {
console.log(teamArr[i]);
}
//Get the team data so that we can pull the logo image.
async function getTeamData(teamID) {
let result = await fetch(`https://www.thesportsdb.com/api/v1/json/1/lookupteam.php?id=${teamID}`)
let teamData = await result.json();
return teamData.teams[0];
}
//Get the info for the teams last game.
async function getLastGame(teamID) {
let result = await fetch(`https://www.thesportsdb.com/api/v1/json/1/eventslast.php?id=${teamID}`)
const lastGames = await result.json();
const lastGame = lastGames.results[0];
return lastGame;
};
//Populate the final scores with new HTML after pulling all the info from the API.
for (let i = 0; i < teamArr.length; i++) {
let homeTeam, awayTeam, homeTeamData, awayTeamData, homeTeamLogo, gameDay;
getLastGame(teamArr[i])
.then(lastGame => {
gameDay = lastGame.dateEvent;
homeTeam = {
name: lastGame.strHomeTeam,
id: lastGame.idHomeTeam,
score: lastGame.intHomeScore,
};
awayTeam = {
name: lastGame.strAwayTeam,
id: lastGame.idAwayTeam,
score: lastGame.intAwayScore
}; //This is all the info we need except for the team icons.
}).then(result => {
homeTeamData = getTeamData(homeTeam.id)
return homeTeamData;
}).then(result => {
homeTeam.logo = result.strTeamBadge;
}).then(() => {
awayTeamData = getTeamData(awayTeam.id)
return awayTeamData;
}).then(result => {
awayTeam.logo = result.strTeamBadge; //After getting who was home and who was away, these let us pull and put the right icon in the table.
}).then(() => {
let html = ` <tr>
<th><img src="%awayImg%" alt="Away" id="${i}-away" class="team-logo"></th>
<th><div class="at-vs">#</div></th>
<th><img src="%homeImg" alt="Home" id="${i}-home" class="team-logo"></th>
</tr>
<tr>
<th><div class="away-score">%awayScore%</div></th>
<th><div class="gameday">%gameDay%</div></th>
<th><div class="home-score">%homeScore%</div></th>
</tr>`;
let newhtml = html.replace(`%awayImg%`, awayTeam.logo + "/preview");
newhtml = newhtml.replace(`%homeImg`, homeTeam.logo + "/preview");
newhtml = newhtml.replace(`%awayScore%`, awayTeam.score);
newhtml = newhtml.replace(`%gameDay%`, gameDay);
newhtml = newhtml.replace(`%homeScore%`, homeTeam.score);
document.querySelector(`.past-games-table`).insertAdjacentHTML(`beforeend`, newhtml);
})
};
It seems that what you want is to make concurrent requests to get the data but still show them in a certain order no matter when they arrive.
Right now you are making all your requests one right after the other and showing the results as you get them. This means that the order in which the results are shown is unpredictable, any of the previous requests can finish before any of the subsequent ones and your data will be shown out of order.
Try running this snippet multiple times, the display order is unpredictable:
Note, in the examples below I'm using some fake functions to represent the requesting of data and displaying it. fakeRequest is a function that can represent all but the last function in your promise chain (all the logic you use to get and build your team data) and fakeDisplay is function that can represent the last part of your chain that appends HTML and displays the data.
const fakeRequest = n => new Promise(
resolve => setTimeout(() => {
console.log(`requesting ${n}`);
resolve(n);
}, Math.random() * 1000)
);
const fakeDisplay = n => console.log(`displaying ${n}`);
// all requests are made one after the other
// and as soon as a request is done, we display
// the data, no matter which request that is
for (let i = 0; i < 3; i++) {
fakeRequest(i).then(fakeDisplay);
}
The main issue here is that your displaying logic is tied together with the logic that gets the team data. Splitting this behavior would allow you to tackle your problem in multiple ways.
One way to fix this is to not make a request (and not display the data) until the previous request+display operation is finished:
const fn = async () => {
// note: using await is only valid in an async function
const fakeRequest = n => new Promise(
resolve => setTimeout(() => {
console.log(`requesting ${n}`);
resolve(n);
}, Math.random() * 1000)
);
const fakeDisplay = n => console.log(`displaying ${n}`);
// make each request and wait for it to finish
// then display the result and start next request
for (let i = 0; i < 3; i++) {
const item = await fakeRequest(i);
fakeDisplay(item);
}
}
fn();
Another way to alleviate this is to wait until all your requests are finished before displaying all the results at once (by using Promise.all):
const fakeRequest = n => new Promise(
resolve => setTimeout(() => {
console.log(`requesting ${n}`);
resolve(n);
}, Math.random() * 1000)
);
const fakeDisplay = n => console.log(`displaying ${n}`);
// make the requests one after the other
// and save all the promises in an array
const promises = []
for (let i = 0; i < 3; i++) {
promises.push(fakeRequest(i));
}
// then use a built-in utility Promise.all to
// display all the results once they all arrive
Promise.all(promises)
.then(results => {
for (const item of results) {
fakeDisplay(item);
}
})
The main issue with this is that your UI will not show anything until all requests are done which can take some time and might not be the ideal experience.
Perhaps your UX would be best if you make all requests around the same time but still show the results as they arrive, in the right the order.
const fn = async () => {
const fakeRequest = n => new Promise(
resolve => setTimeout(() => {
console.log(`requesting ${n}`);
resolve(n);
}, Math.random() * 1000)
);
const fakeDisplay = n => console.log(`displaying ${n}`);
// make the requests one after the other
// and save all the promises in an array
const promises = []
for (let i = 0; i < 3; i++) {
promises.push(fakeRequest(i));
}
// loop through the promises and resolve each
// one and display the data
for await (const item of promises) {
fakeDisplay(item);
}
}
fn();
You are mixing up two patterns that achieve the same thing. Here is your code refactored with async await
//Define the teams I want scores from and their API reference numbers.
let teams = new Map();
teams.set("Patriots", 134920);
teams.set("Steelers", 134925);
teams.set("Bruins", 134830);
teams.set("Penguins", 134844);
teams.set("Celtics", 134860);
teams.set("Red Sox", 135252);
teams.set("Huskers", 136923);
let teamArr = Array.from(teams.values());
for (i = 0; i < teamArr.length; i++) {
console.log(teamArr[i]);
}
//Get the team data so that we can pull the logo image.
async function getTeamData(teamID) {
let result = await fetch(`https://www.thesportsdb.com/api/v1/json/1/lookupteam.php?id=${teamID}`)
let teamData = await result.json();
return teamData.teams[0];
}
//Get the info for the teams last game.
async function getLastGame(teamID) {
let result = await fetch(`https://www.thesportsdb.com/api/v1/json/1/eventslast.php?id=${teamID}`)
const lastGames = await result.json();
const lastGame = lastGames.results[0];
return lastGame;
};
//Populate the final scores with new HTML after pulling all the info from the API.
for (let i = 0; i < teamArr.length; i++) {
let homeTeam, awayTeam, homeTeamData, awayTeamData, homeTeamLogo, gameDay;
const lastGame = await getLastGame(teamArr[i]);
gameDay = lastGame.dateEvent;
homeTeam = {
name: lastGame.strHomeTeam,
id: lastGame.idHomeTeam,
score: lastGame.intHomeScore,
};
awayTeam = {
name: lastGame.strAwayTeam,
id: lastGame.idAwayTeam,
score: lastGame.intAwayScore
}; //This is all the info we need except for the team icons.
homeTeamData = await getTeamData(homeTeam.id);
homeTeam.logo = homeTeamData.strTeamBadge;
awayTeamData = await getTeamData(awayTeam.id);
awayTeam.logo = awayTeamData.strTeamBadge; //After getting who was home and who was away, these let us pull and put the right icon in the table.
let html = ` <tr>
<th><img src="%awayImg%" alt="Away" id="${i}-away" class="team-logo"></th>
<th><div class="at-vs">#</div></th>
<th><img src="%homeImg" alt="Home" id="${i}-home" class="team-logo"></th>
</tr>
<tr>
<th><div class="away-score">%awayScore%</div></th>
<th><div class="gameday">%gameDay%</div></th>
<th><div class="home-score">%homeScore%</div></th>
</tr>`;
let newhtml = html.replace(`%awayImg%`, awayTeam.logo + "/preview");
newhtml = newhtml.replace(`%homeImg`, homeTeam.logo + "/preview");
newhtml = newhtml.replace(`%awayScore%`, awayTeam.score);
newhtml = newhtml.replace(`%gameDay%`, gameDay);
newhtml = newhtml.replace(`%homeScore%`, homeTeam.score);
document.querySelector(`.past-games-table`).insertAdjacentHTML(`beforeend`, newhtml);
};
One thing you can do in async functions is to await the resolution of a Promise. In a for loop, this means you won't continue executing the loop until your previous iteration has resolved. Here's a trivial example. You will likely have a similar result if you aggregate all your fetches in an array (e.g., [getLastGame(teamArr[0]), getLastGame(teamArr[1]), getLastGame(teamArr[2]), etc.]) and then loop over that array using await on each element.
const timed = (id) => new Promise(res => {
setTimeout(() => res(id), Math.random() * 2000);
})
const func = async () => {
const promises = [timed(1), timed(2), timed(3), timed(4)];
for (let promise of promises) {
const returned = await promise;
console.log(returned);
}
}
func();
Something else you can consider is, again aggregating all your promises, but then using Promise.all to await the resolution of all of the promises to then do something when all your data have arrived:
Promise.all([getLastGame(teamArr[0]), getLastGame(teamArr[1], etc.)])
.then(all => {
// the `all` variable will contain an array of all resolved promises
})

Fetching data in the loop

So I'm trying to connect to external server called Pexels to get some photos. I'm doing that from node.js but it is just a javascript issue. Pexels unfortunately lets user to download object with only 40 pictures per page.
https://api.pexels.com/v1/curated?per_page=40&page=1 // 40 is maximum
But actually I need more then that. I'd like to get 160 results, ie. to combine all first four pages. In order to do that I tried looping the request:
let pexelsData = [];
for(let i = 1; i < 5; i++) {
const randomPage = getRandomFromRange(1, 100); //pages should be randomized
const moreData = await axios.get(`https://api.pexels.com/v1/curated?per_page=40&page=${randomPage}`,
createHeaders('bearer ', keys.pexelsKey));
pexelsData = [ ...moreData.data.photos, ...pexelsData ];
}
Now I can use pexelsData but it work very unstable, sometimes it is able to get all combined data, sometimes it crashes. Is there a correct and stable way of looping requests?
You work with 3rd party API, which has rate limits. So you should add rate limits to your code. The simplest solution for you is using p-limit or similar approach form promise-fun
It will looks like that:
const pLimit = require('p-limit');
const limit = pLimit(1);
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => doSomething())
];
(async () => {
// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);
})();
you can break it into functions like..
let images=[];
const getResponse = async i=> {
if(i<5)
return await axios.get(`https://api.pexels.com/v1/curated?per_page=40&page=${i}`)
}
const getImage = (i)=>{
if(i<5){
try {
const request = getResponse(i);
images = [...images,...request];
// here you will get all the images in an array
console.log(images)
getImage(++i)
} catch (error) {
console.log("catch error",error)
// getImage(i)
}
}
}
getImage(0); //call initail

Categories