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],
}
})
Related
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()
} )
I am learning ReactJS and trying to update the parent props with the updated state of ingredients from the child component. The setUserIngredients is called and updated ingredients are being passed to parent.
Code :
const [userIngredients, setUserIngredients] = useState([]);
const removeIngredientHandler = id => {
setLoading(true);
fetch(`https://***************.com/ingredients/${id}.json`,{
method:'DELETE'
}).then(response=>{
setLoading(false);
setUserIngredients(prevIngredients =>
prevIngredients.filter(ingredient =>{
return (ingredient.id !== id)
//return ingredient;
})
);
**props.ingredients(userIngredients);**
//userIngredients is still having old value
//need to check on this
}).catch(error => {
setError(error.message);
})
};
The problem is that userIngredients is a variable that is created when the component renders, set to a version fo the state when that component renders. And when you start an asynchronous operation (like a fetch) the callback you pass to that operation will be bound the values from when that callback was created.
The fix here is pretty simple. In the spot where you calculate your new ingredients, simply execute whatever callback you need before returning the value to be stored in the state.
Something like:
fetch(`https://***************.com/ingredients/${id}.json`, {
method: 'DELETE',
}).then(response => {
setLoading(false)
setUserIngredients(prevIngredients => {
// Figure out the new ingredients
const newIngredients = prevIngredients.filter(ingredient => ingredient.id !== id)
// Call your callback with the new ingredients
props.ingredients(newIngredients)
// Return the new ingredients to be stored in your state
return newIngredients
})
})
I'm creating a react-native app and I need one of my components to use a axios get request when I do an action on another component. But the problem is that my component that I need an axios get request from is not being passed any props and the current state and new state is an array of 20+ objects with each at least 10 key value pairs. So I would need a component did update with a good if statement to not go into an infinite loop. I can't do an if statement with prevState to compare with current state because there is only a minor change happening in state. So I need to know how to stop the component Did Update from having an infinite loop.
state = {
favouriteData: []
}
componentDidMount () {
this.getFavouriteData()
}
componentDidUpdate (prevProps, prevState) {
if (this.state.favouriteData !== prevState.favouriteData){
this.getFavouriteData()
}
}
getFavouriteData = () => {
axios.get('http://5f46425d.ngrok.io')`enter code here`
.then(response => {
const data = response.data.filter(item => item.favourite === true)
this.setState({
favouriteData: data
})
})
}
The issue is that you are trying to compare 2 object references by doing the following. It will always return since the references are always different.
if (this.state.favouriteData !== prevState.favouriteData) {...}
To make life easier, we can use Lodash(_.isEqual) to deal with deep comparison of objects.
state = {
favouriteData: []
}
componentDidMount () {
this.getFavouriteData()
}
componentDidUpdate (prevProps, prevState) {
this.getFavouriteData(prevState.favouriteData)
}
getFavouriteData = (prevData) => {
axios.get('http://5f46425d.ngrok.io')
.then(response => {
const data = response.data.filter(item => item.favourite === true);
// compare favouriteData and make setState conditional
if (!prevState || !_.isEqual(prevData, data)) {
this.setState({
favouriteData: data
})
}
})
}
You should use react-redux to avoid this kind of issues. Assuming you are not using flux architecture, you can pass this.getFavouriteData() as props to the other component like:
<YourComponent triggerFavouriteData = {this.getFavouriteData}/>
I am having this scenario, where I have a dropdown which pulls the accounts of a user and then based on its selection the content of the page changes. I am trying an approach which is shown below , It seems like setState does not get invoked properly or may be calling sequence may be wrong. Not really getting what is wrong. Changing drop down value does not update the content .
Help would be appreciated.
import * as React from 'react';
import Select from 'semantic-ui-react/dist/commonjs/addons/Select';
interface IState{
accountDetails[], // stores all details w.r.t an account
userAccounts: [], // stores all accounts w.r.t a user
selectedAccount : string, // selected account from the dropdown
[x: string] : any,
}
export default class App extends React.Component<{},IState> {
constructor(props:any){
super(props);
this.state = {
accountDetails: [],
userAccounts: [],
selectedAccount: ''
}
}
dropdownChange = (event: React.SyntheticEvent<HTMLElement>, data:any) => {
this.setState(prevState => ({
selectedAccount: data.value
}), () => {});
}
async componentDidMount()
{
await this.fetchUserAccounts();
}
fetchUserAccounts = async() => {
//fetch API call for getting all user accounts by passing a user ID
// I am able to get the data
fetch('/api/fetch/accounts'
.then(response => response.json())
.then(data => this.setState({ userAccounts: data}));
this.fetchAccountDetails();
}
fetchAccountDetails = async() =>
{
let URL = "/api/fetch/account?accountId=" +this.state.selectedAccount;
fetch('URL'
.then(response => response.json())
.then(data => this.setState({ accountDetails: data}));
}
render() {
return(
<div>
<Select
options={this.state.userAccounts}
name="selectedAccount"
value={this.state.selectedAccount}
onChange={this.dropdownChange}
/>
// component to display the details
<DetailComponent accounts={this.state.accountDetails} />
</div>
)
}
}
You need to call fetchAccountDetails right after changing the state, for the function to invoke using the latest state that the dropdown has changed:
dropdownChange = (event: React.SyntheticEvent<HTMLElement>, data:any) => {
this.setState(prevState => ({
selectedAccount: data.value
}), () => { this.fetchAccountDetails() });
}
the setState function is asynchronous, which means you need to take special precaution when updating state and expecting to use the updated value in the state synchronously. For this reason, the setState function has a second parameter which allows you to specify a callback which is executed when the state has actually been updated. This is where you would need to call the fetchAccountDetails function.
You can find the setState callback parameter described in the React docs here.
But you will have trouble compiling this either way. (a) Your interface should end each member declaration with a semicolon, not a comma. (b) You're missing a ")" on each line with a call to fetch. (c) In the fetchAccountDetails function you declare the URL variable, but then you pass a string 'URL' to the fetch function, not the variable.
Here is a working code sandbox showing the fix to your question, and the syntax fixes. I've commented out the calls to the fetch function since they will fail anyway.
https://codesandbox.io/s/thirsty-rain-2u69e
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() });