React setState in a map function - reactjs

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() });

Related

Passing search results to a component in React [duplicate]

This question already has answers here:
How to pass async state to child component props?
(2 answers)
Closed 10 days ago.
I've built a React app to search recordings, that has the following structure:
SearchScreen => SearchParms
=> SearchResults
SearchScreen is rendered like this:
return(
<Fragment>
<div className='containerCR'><SearchParms getRecordingsHandle = {this.handleGetRecordings} />
<SearchResults recordings = {this.state.recordings}/>
</Fragment>)
A button on the Search Parms screen calls this.handleGetRecordings, which calls an async function to get records, adds them to state.recordings and then passes them to the SearchResults screen.
Problem is, because it's async it passes the recordings object before it's populated.
handleGetRecordings sets the state for the search parameter ani. It needs to be done in a then because it doesn't set it immediately:
handleGetRecordings = (ani) =>
{
console.log(this.state.ani);
console.log("Passed value: ", ani);
this.setState({
ani:ani
}, () => {
console.log("Set state finished:", this.state.ani);
this.fetchRecordings();
});
}
The fetchRecordings function gets the data and then calls setRecordings:
fetchRecordings = async () => {
console.log("getting recordings for: ", this.state.ani);
const myInit = {
headers: {},
response: true,
queryStringParameters: {
ani: encodeURIComponent(this.state.ani)
}
};
console.log("Params", myInit);
API.get(myAPI, path, myInit)
.then(response => {
console.log("Response:", response)
let newRecordings = this.state.recordings;
newRecordings.push(response)
this.setRecordings(newRecordings[0].data)
})
.catch(error => {
console.log(error)
})
}
setRecordings writes the list of recordings to the state, ready to pass to the SearchResults page, except by the time it gets here its missed its chance!
setRecordings =(recordings) =>
{
console.log("Passed value: ", recordings);
this.setState({
recordings:recordings
}, () => {
console.log("Current State: ", this.state.recordings);
});
}
What's the correct way of doing this?
Reading the state the way you're doing here, is not guaranteed to give you the latest state possible.
let newRecordings = this.state.recordings
In order to use your old state to create a new one, you need to pass an updater function to setState as the first argument. Look at the examples here in React docs: https://reactjs.org/docs/react-component.html#setstate
Then you can create your new state based on your old state and set it as the new state by returning it from your updater function. This way you can be sure that your new state is what you expect it to be.
Moreover, since your state is an array, its always passed/refered to by reference and React always suggests to not mutate a state (here, we're mutating it by pushing a value to it), but instead, create a new one by cloning the previous one. So the final setState should look like this:
this.setState((prevState) => {
return {
recordings: [...prevState.recordings, response],
}
})

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.

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.

setState is not updating in my other method

I'm very new to react and i'm confused why my state is not updated in another method of mine see example below.
fetchMovies = () => {
const self = this;
axios.get("https://api.themoviedb.org/3/trending/movie/day?api_key=XXXXXXX")
.then(function(response){
console.log(response.data)
self.setState({
collection: response.data.results
})
console.log(self.state.collection)
});
}
makeRow = () => {
console.log(this.state.collection.length);
if(this.state.collection.length !== 0) {
var movieRows = [];
this.state.collection.forEach(function (i) {
movieRows.push(<p>{i.id}</p>);
});
this.setState({
movieRow: movieRows
})
}
}
componentDidMount() {
this.fetchMovies();
this.makeRow();
}
When inside of fetchMovies function i can access collection and it has all the data but this is the part i can't understand in the makeRow function when i console log the state i would of expected the updated state to show here but it doesn't i'm even executing the functions in sequence.
Thanks in advance.
the collection is set after the async call is resolved. Even though makeRow method is called after fetchMoview, coz of async call, u will never know when the call will be resolved and collection state will be set.
There is no need to keep movieRows in the state as that is just needed for rendering. Keeping html mockup in the state is never a good idea.
So u should just call fetchMoviews in the componentDidMount and render the data in as follows:
render() {
const { collection } = this.state;
return (
<>
{
collection.map(c => <p>{c.id}</p>)
}
</>
)
}
make sure the initial value for collection in the state is [] .
The setState() documentation contains the following paragraph:
Think of setState() as a request rather than an immediate command
to update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
To access the modified state you need to use the function signature setState(updater, [callback]), so in your case it should be;
self.setState({
collection: response.data.results
}, () => { // Will be executed after state update
console.log(self.state.collection)
// Call your make row function here and remove it from componentDidMount if that is all it does.
self.makeRow()
} )

What is the best way to mount a component when you need a prop from another one

I have two actions. Sometimes the value returns undefined so instead of receiving the users post I receive all the post. I need to find a better way to get my functions to work together.
Right now I have:
componentDidMount(){
this.props.fetchUser();
let userId = this.props.user.map((user) => (user.id));
console.log(userId)
this.props.fetchPostsFromUser(userId);
}
Because it can't get the userId right away so it won't add the parameter to the end of my user search so it gives me all post.
If I try to do something else like:
componentDidMount(){
this.props.fetchUser();
}
componentWillReceiveProps(){
let userId = this.props.user.map((user) => (user.id));
console.log(userId)
this.props.fetchPostsFromUser(userId);
}
It will loop give me the post but loop infinitly in the console.
Also putting the code below in the render will also make it loop forever.
let userId = this.props.user.map((user) => (user.id));
console.log(userId)
this.props.fetchPostsFromUser(userId);
What is the best way to go about this so it works how I want it to.
EDIT
export const fetchUser = () => dispatch => {
Promise.all([fetch('http://10.6.254.22:5000/userinformation/3')])
.then(([res1]) => {
return Promise.all([res1.json()])
})
.then(([user]) => dispatch ({
// set state in here
type: FETCH_USER,
payload: user
// console.log(res1)
// this.setState({ results: res1, posts: res2 })
// this.setState({ posts: res2 })
}));
}
You want to fetch posts 1 time, but can't do it on mount because you don't have the userId yet. If you do it in componentWillReceiveProps you get an infinite loop -- because fetching posts updates props.
You need to run some code when the component updates, but only when you have a user id and didn't previously have a user id. componentWillReceiveProps is discouraged and deprecated so we will use componentDidUpdate which gets prevProps as it's first paramater.
Using componentDidUpdate:
componentDidUpdate(prevProps) {
let prevUserId = prevProps.user.map((user) => (user.id));
let userId = this.props.user.map((user) => (user.id));
if(!prevUserId && userId) {
this.props.fetchPostsFromUser(userId);
}
}
If you're curious how this might look using hooks, here's an example:
const userId = props.user.map((user) => (user.id))
React.useEffect(() => {
if(userId) {
props.fetchPostsFromUser(userId)
}
}, [userId])
The hook will only be called when userId has changed value, and will only fetch posts if there is a userId

Resources