Async function runs continuously - reactjs

I'm using the following code to fetch data from two different sources in react using hooks.
const [ permissionTree, setPermissionTree ] = useState([]);
const [ availablePermissionsInRole, setAvailablePermissionsInRole ] = useState<Permission[]>([]);
const getAllPermissions = (): void => {
getPermissionList()
.then(response => {
if (response.status === 200 && response.data && response.data instanceof Array) {
const permissionStringArray = response.data;
let permissionTree: Permission[] = [];
permissionTree = permissionStringArray.reduce((arr, path) => addPath(
path, path.resourcePath.replace(/^\/|\/$/g, "").split('/'), arr,
), []);
setPermissionTree(permissionTree);
}
})
.catch(error => {
//Handle Permission Retrieval Properly
})
}
/**
* Retrieve permissions for a given role if in Role edit mode.
*/
useEffect(() => {
if (isEdit && roleObject) {
getPermissionsForRole(roleObject.id)
.then(response => {
if (response.status === 200 && response.data instanceof Array) {
const permissionsArray: Permission[] = [];
response.data.forEach(permission => {
permissionsArray.push({
id: permission,
isChecked: false,
fullPath: permission
})
})
setAvailablePermissionsInRole(permissionsArray);
getAllPermissions();
}
})
.catch(error => {
//Handle Role Retrieval Properly
})
} else {
getAllPermissions();
}
}, [])
The first async call getPermissionsForRole returns a string array and the second getAllPermissions returns an array of objects which I then parse on to a util method to create a different array of objects.
With an empty array as the second argument in useEffect the continuous async call is stopped but when I check availablePermissionsInRole inside the getAllPermissions method, it's empty. When I pass availablePermissionsInRole as the second argument the continuous loop occurs.
What am I doing wrong in this code. Please guide me since I'm new to react hooks.

I found a solution for the infinite loop of the useEffect and since I pass the availablePermissionsInRole as the second argument, react look only at the array reference and treats it as a new array hence the infinite loop. As a fix for this I first passed the following as the argument.
availablePermissionsInRole.length
which will stay the same unless the backend sends a new array element. Then again the strings inside the array could change without the length being changed so I used the following as the arguement which fixes my issue.
availablePermissionsInRole.toString()
Which then changes if a new string is also retrieved.
Thank you all for the help but do point out if there is any errors in the approach I have used.

Related

Const is not defined -> Yet standard solutions don't work

I want to display a mapped list where "UserName" is an entry value from a Firebase Realtime Database corresponding to the author of each entry.
The following code, inside the get(UsernameRef).then((snapshot) =>{}) scope, returns an undefined reference error as expected, 'UserName' is assigned a value but never used and 'UserName' is not defined
const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
const RecipeLibraryRef = ref(db, "Recipes/");
onValue(RecipeLibraryRef, (snapshot) => {
const RecipeLibrary = [];
snapshot.forEach((child) => {
const AuthorUserId = child.key;
child.forEach((grandChild) => {
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
get(UserNameRef).then((snapshot) => {
const UserName = snapshot.val();
});
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
...grandChild.val(),
});
});
});
setRecipeLibrary(RecipeLibrary);
console.log({ RecipeLibrary });
});
}, []);
I've tried:
Using a React state to pass the variable -> Can't use inside React useEffect
Exporting and Importing a separate function that returns the desired UserName -> return can only be used in the inner scope
Moving the list .push inside the Firebase get scope -> React.useState can no longer access the list
I'm hoping there is a simple solution here, as I am new.
Your time and suggestions would mean a lot, thank you!
Update:
I got the RecipeLibrary array to contain the desired "UserName" entry, named author by moving the array .push inside the .then scope. Here is a log of that array at set (line 59) and at re-render (line 104).
child.forEach((grandChild) => {
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
get(UserNameRef).then((snapshot) => {
const UserName = snapshot.val();
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
authorId: AuthorUserId,
...grandChild.val(),
});
});
});
});
setRecipeLibrary(RecipeLibrary);
console.log(RecipeLibrary);
However, now the mapped list is not rendering at all on screen.
Just some added context with minimal changes to original code, been stuck on this so long that I'm considering a full re-write at this point to jog my memory. Oh and here is the bit that renders the mapped list in case:
<Box width="75%" maxHeight="82vh" overflow="auto">
{RecipeLibrary.map((RecipeLibrary) => (
<Paper
key={RecipeLibrary.name}
elevation={3}
sx={{
etc...
This is a tricky one - the plainest option might be to move push() and setRecipeLibrary() inside the then() callback so they're all within the same scope, but that would have some terrible side effects (for example, triggering a re-render for every recipe retrieved).
The goal (which you've done your best to achieve) should be to wait for all the recipes to be loaded first, and then use setRecipeLibrary() to set the full list to the state. Assuming that get() returns a Promise, one way to do this is with await in an async function:
const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
const RecipeLibraryRef = ref(db, "Recipes/");
onValue(RecipeLibraryRef, (snapshot) => {
// An async function can't directly be passed to useEffect(), and
// probably can't be accepted by onValue() without modification,
// so we have to define/call it internally.
const loadRecipes = async () => {
const RecipeLibrary = [];
// We can't use an async function directly in forEach, so
// we instead map() the results into a series of Promises
// and await them all.
await Promise.all(snapshot.docs.map(async (child) => {
const AuthorUserId = child.key;
// Moved out of the grandChild loop, because it never changes for a child
const UserNameRef = ref(db, "Account/" + AuthorUserId + "/username");
// Here's another key part, we await the Promise instead of using .then()
const userNameSnapshot = await get(UserNameRef);
const UserName = userNameSnapshot.val();
child.forEach((grandChild) => {
RecipeLibrary.push({
name: grandChild.key,
author: UserName,
...grandChild.val(),
});
});
}));
setRecipeLibrary(RecipeLibrary);
console.log({ RecipeLibrary });
};
loadRecipes();
});
}, []);
Keep in mind that Promise.all() isn't strictly necessary here. If its usage makes this less readable to you, you could instead execute the grandChild processing in a plain for loop (not a forEach), allowing you to use await without mapping the results since it wouldn't be in a callback function.
If snapshot.docs isn't available but you can still use snapshot.forEach(), then you can convert the Firebase object to an Array similar to Convert A Firebase Database Snapshot/Collection To An Array In Javascript:
// [...]
// Change this line to convert snapshot
// await Promise.all(snapshot.docs.map(async (child) => {
await Promise.all(snapshotToSnapshotArray(snapshot).map(async (child) => {
// [...]
// Define this somewhere visible
function snapshotToSnapshotArray(snapshot) {
var returnArr = [];
snapshot.forEach(function(childSnapshot) {
returnArr.push(childSnapshot);
});
return returnArr;
}
Note that if get() somehow doesn't return a Promise...I fear the solution will be something less straightforward.

How correctly fetch and save data inside an array using useEffect

I am using React and asynchronously fetching data from the blockchain with useEffect.
What I do not understand is that while the console.log inside the map function works and it prints the right data, supposedly it should save that data inside data array, but when I log data outside the map (meaning it should have finished to save the data) I get an array of undefined values.
Code:
useEffect(() => {
const data = mainnet.Vaults.map((pool) => {
const loadLendingData = async () => {
const dataPool = await getPoolInfo(pool);
console.log('dataPool', dataPool) //all good it prints everything
return dataPool
};
loadLendingData();
})
console.log('data', data) //[undefined, undefined, undefined and so on]
setData(data)
}, []);
Why is this happening? What am I doing wrong?
EDIT:
I fixed the problem by storing data with useState for each loop in map.
Code:
useEffect(() => {
mainnet.Vaults.map((pool) => {
const loadLendingData = async () => {
const dataPool = await getPoolInfo(pool);
setLendingData((prevPool) => [...prevPool, dataPool])
};
loadLendingData();
})
}, []);
But still I'd like to understand why the first example didn't work.
I see that you are not returning anything from the map function, that's why the array has undefined elements outside the map function.
return loadLendingData()
This should solve the problem for you.

React keep old state - new state not updated

In a React project, I have a state gameResults with a array of games, and I have a function to get the list of games based on a query :
useEffect(() => {
const timeoutId = setTimeout(() => {
if (gameQuery.length > 0) {
axios.get(`/api/games/${gameQuery}`).then((response) => {
const igdbGames: IGDBGame[] = response.data.games;
const formatedGames = formatGames(igdbGames);
setGameResults(formatedGames);
});
}
}, 300);
return () => clearTimeout(timeoutId);
}, [gameQuery]);
For each game, I don't have the cover, so I get the cover for each game :
const loadGamesImages = async () => {
for (let i = 0; i < gameResults.length; i++) {
axios
.get(`/api/cover/${gameResults[i].id}`)
.then((response) => {
const coverUrl: IGDBCover = response.data.covers[0];
const newGame = {
...gameResults[i],
cover: coverUrl.url.replace("//", "https://"),
};
const newGames = gameResults.filter(
(game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);
})
.catch((error) => {
console.log("error", error);
});
await sleep(300);
}
console.log("finish");
};
useEffect(() => {
loadGamesImages();
}, [gameResults.length]);
Here is my problem : when React update the state, the old state is not there anymore. I explain : for the first cover, it's ok the new state has the first game covered. But when he make a new state for the second game, as you can see i get the gameResults state, but in this one the first game has no cover anymore.
Here is the result :
What have I done wrong ?
Each one of your looped asynchronous calls closes over the initial binding of the stateful gameResults - and gameResults starts out empty. For example, with the first Promise that resolves, these line:
const newGames = gameResults.filter(
(game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);
have the gameResults refer to the empty array, so setGameResults properly spreads the empty array plus the just-added newGame.
But then on further Promise resolutions, they also close over the initially-empty gameResults - all the async calls happened before the component re-rendered.
Use a callback instead, so that the async calls don't overwrite each other:
setGameResults((gameResults) => {
const newGames = gameResults.filter(
(game) => game.id !== newGame.id
);
return [...newGames, newGame];
});
(also note that there's no need to explicitly note the type of a parameter that TS can already infer automatically: (game: Game) can be just game)
Once this is working, I'd also suggest tweaking your code so that, when the effect hook runs again, only covers that have not been retrieved yet get requested again. This'll save you from unnecessarily making duplicate requests.

AsyncStorage complains of array and object incompatibility during mergeItem

I'm trying to figure out why AsyncStorage in a React Native app refuses to do a mergeItem with my data. My function looks like this:
const arrToObj = (array) =>
array.reduce((obj, item) => {
obj[Object.keys(item)[0]] = Object.values(item)[0]
return obj
}, {})
export const addThingToThing = async (title, thing) => {
try {
let subThings = []
let things = {}
await AsyncStorage.getItem(THINGS_STORAGE_KEY)
.then((things) => {
let subThings = []
Object.values(JSON.parse(decks))
.map((thing) => {
if (Object.keys(thing)[0] === title) {
subThings = [...Object.values(thing)[0].subThings, subThing]
}
})
return { decks, subThings }
})
.then(({ decks, subThings }) => {
const obj = {
...arrToObj(JSON.parse(things)),
[title]: {
subThings
}
}
console.log(JSON.stringify(obj))
AsyncStorage.mergeItem(THINGS_STORAGE_KEY,
JSON.stringify(obj))
})
} catch (error) {
console.log(`Error adding thing to thing: ${error.message}`)
}
}
When I do the thing that executes this I get:
13:35:52: {"test":{"subThings":[{"one":"a","two":"a"}]},"test2":{"title":"test2","questions":[]}}
13:35:55: [Unhandled promise rejection: Error: Value [{"test":{"title":"test","subThings":[]}},{"test2":{"title":"test2","subThings":[]}}] of type org.json.JSONArray cannot be converted to JSONObject]
Which is confusing, because when the data is printed out it's an object with {...}, but AsyncStorage shows an array with [...]. Is there something I'm missing? This seems pretty dumb to me and I can't seem to figure out how to get RN to play nice.
PS. IMO the structure of the data is gross, but it's what I'm working with. I didn't decide on it, and I can't change it.
I recall working with AsyncStorage, and it is fundamentally different than localStorage because it returns Promises.
Your code looks fine which makes this super confusing, but I am suddenly suspecting that the problem may be due to a missing await keyword.
try:
.then(async ({ decks, subThings }) => {
const obj = {
...arrToObj(JSON.parse(things)),
[title]: {
subThings
}
}
console.log(JSON.stringify(obj))
// We are adding await here
await AsyncStorage.mergeItem(THINGS_STORAGE_KEY, JSON.stringify(obj))
})
It may be a classic "not waiting for the Promise to resolve" before moving on, async problem. Don't delete this question if so. It will be helpful for others including probably myself in the future.
Here's what I think is happening:
JavaScript throws AsyncStorage.mergeItem() into the function queue
There is no await for this function that returns a Promise
The interpreter does not wait for it to resolve and immediately moves to the next line of code
There is no next line of code, so it returns from the then() block 0.1ms later (keep in mind it is implicitly doing return undefined which inside an async function is the same as resolve(undefined), with the point being that it is trying to resolve the entire chain back up to await AsyncStorage.getItem(THINGS_STORAGE_KEY)
obj gets garbage collected 0.1ms after that
AsyncStorage.mergeItem() observes a falsy value where it was expecting obj, but I don't actually know for certain. It may not be doing step 5 or 6 and instead detecting that mergeItem is still pending when it tries to resolve the getItem chain, either way:
AsyncStorage then gives a confusing error message
What I ended up using to handle the promises correctly is:
export const addThingToThing = async (title, thing) => {
try {
AsyncStorage.getItem(THINGS_STORAGE_KEY, (err, things) => {
let subThings = []
Object.values(JSON.parse(decks))
.map((thing) => {
if (Object.keys(thing)[0] === title) {
subThings = [...Object.values(thing)[0].subThings, subThing]
}
})
const obj = {
[title]: {
title,
subThings
}
}
console.log(JSON.stringify(obj))
AsyncStorage.mergeItem(THINGS_STORAGE_KEY,
JSON.stringify(obj))
})
} catch (error) {
console.log(`Error adding thing to thing: ${error.message}`)
}
}
I saw some other similar examples online that used .done() that looked identical to what I'd done other than that call. That may have been something to try as well, but this worked for me.

React setState in a map function

I cannot wrap my head around the issue below.
The issue relates to the asynchronous setState dimension. Usually I use the callback, but doesn't seem appropriate here.
My goal is to create a state (that I will be able to sort) which is obtained by iterating on different states which are themselves created in a map.
The function below calls my different methods, the ones we're interested in are the 2 last ones. getUserPoints and sortArrayforUserRank.
getPlayersByUser = () => {
database
.ref(`pools/${this.state.selectedValue}`)
.once("value")
.then(data => {
for (let item in data.val()) {
this.setState({
users: this.state.users.concat([item])
});
this.setState({ [item]: data.val()[item] });
}
})
.then(this.makePlayersArray)
.then(this.getUserPoints)
.then(this.sortArrayforUserRank);
getUserPoints = () => {
this.state.users.map(user => {
// Create the dynamic name of the state, 1 for each user
let userPoints = `${user}points`;
// initializing the state for userPoint to be at 0 for future calculation
this.setState({ [userPoints]: 0 });
this.state[user].map(player => {
database
.ref(`players/${player}`)
.child("points")
.once("value")
.then(data => {
let points = parseInt(data.val());
this.setState(state => ({
[userPoints]: points + state[userPoints]
}));
});
});
});
The getUserPoints allow me to dynamically create the state.userPoints summing all the points from the players for each user.
Then I was expecting the sortArrayforUserRank below to use the updated state.userPoints to create my final userArrayPoints state.
sortArrayforUserRank = () => {
this.state.users.map(user => {
let userPoints = `${user}points`;
this.setState(state => ({
userArrayPoints: state.userArrayPoints.concat([
{ [user]: state[userPoints] }
])
}));
});
Currently the userArrayPoints gets populated with 4 objects {[user]:0} instead of the final sum of points for each user. The issue there is that sortArrayforUserRank gets called before the previous setState are done
I would have loved to use the setState callback in getUserPoints but since I'm in the player map function it will get called for each player whereas I want to handle it at the user lvl to have the final sum of points.
I tried to use componentDidUpdate, and made sur to use functionnal setState as per those articles but couldn't figure it out.
https://medium.com/#shopsifter/using-a-function-in-setstate-instead-of-an-object-1f5cfd6e55d1
https://medium.freecodecamp.org/functional-setstate-is-the-future-of-react-374f30401b6b
Your help will be glady appreciated,
thanks
You can't do what you are trying here with setState because it is asynchronous and will conflict with the different states available on each iteration in that for () loop.
What you can do is extract the state first, manipulate it as needed, then run setState (at least in this one below)
.then(data => {
// Pull out what you want to mess with here first
const users = [ ...this.state.users ];
const dataValObj = data.val();
// Then use spread operator (or Object.assign/Array.concat)
this.setState({
users: [
...users,
...Object.keys(dataValObj)
],
...dataValObj
});
})
And it seems you followed a similar pattern throughout the code. Try and apply what I've done here to the other areas that are using loops with setState inside them.
found one way to do it using promise all below. It allows me to remove the setState from the loop and directly work on it instead of have to rely on 2 setstate. Both comments/answer helped me to process that,
getUserPoints = () => {
this.state.users.map(user => {
// Create the dynamic name of the state, 1 for each user
let userPoints = `${user}points`;
// initializing the state for userPoint to be at 0 for future calculation
this.setState({ [userPoints]: 0 });
let userCounter = 0;
let promiseArrays = [];
this.state[user].map(player => {
let promise = database
.ref(`players/${player}`)
.child("points")
.once("value")
.then(data => {
let points = parseInt(data.val());
return (userCounter = userCounter + points);
});
promiseArrays.push(promise);
});
Promise.all(promiseArrays).then(() =>
this.setState({
userArrayPoints: this.state.userArrayPoints.concat({
[user]: userCounter
})
})
);
});
};
Just use second argument in this.setState.
Second argument is a function that will be called after seting State.
this.setState({
name:value
},() => {this.nameOfTheFunctionYouWantToRunNext() });

Resources