问题
I've played around with the code for a while now. I'm grabbing data from Firebase and populating a list of objects from that data onto this UserProfile page. I just want it to work so that when I go on the profile page, a list of their items is listed there.
The problem is that with this version of the code I have to click on the profile link twice for the items to show up, but the display name shows up fine. I know setState is asynchronous. I've tried setting the state in a callback in setState. I've tried checking if the snapshot exists beforehand. I've tried componentDidMount and componentDidUpdate. None of these things have helped, I just end up with this.state.items.map can't be called or newState is some empty, weird thing. I don't know where I'm going wrong now.
When I console debug this, it looks like setState is being called when nothing has been fetched from Firebase so far and never gets called later.
Maybe something is wrong with how I set newState because when I console log newState is empty when I'm trying to first set it. I just don't know when to set this at the appropriate time for the first render.
class UserProfile extends Component {
constructor(props){
super(props)
this.state = {
redirect: false,
userId: this.props.match.params.id,
displayName: "",
items: []
}
this.userRef = firebaseDB.database().ref(`/Users/${this.state.userId}`)
this.usersItemsRef = firebaseDB.database().ref(`/Users/${this.state.userId}/items`)
}
componentWillMount() {
this.userRef.on('value', snapshot => {
this.setState({
displayName: snapshot.val().displayName
});
});
this.usersItemsRef.on('value', snapshot => {
let usersItems = snapshot.val();
let newState = [];
for (let userItem in usersItems) {
var itemRef = firebaseDB.database().ref(`/items/${userItem}`)
itemRef.once('value', snapshot => {
var item = snapshot.val()
newState.push({
id: itemRef.key,
title: item.title
});
})
}
this.setState({
items: newState
})
});
}
componentWillUnmount() {
this.userRef.off();
this.usersItemsRef.off();
}
render() {
return (
<div style={{marginTop: "100px"}}>
<h1>{this.state.displayName}</h1>
<Link className="pt-button" aria-label="Log Out" to={"/submit_item/"+this.state.userId} >Submit an item</Link>
<section className='display-itemss'>
<div className="wrapper">
<ul>
{this.state.items.map((item) => {
return (
<li key={item.id}>
<h3>{item.title}</h3>
</li>
)
})}
</ul>
</div>
</section>
</div>
);
}
}
回答1:
Data is loaded from Firebase asynchronously, since it may take an undetermined amount of time. When the data is available, Firebase calls the callback function you passed in. But by that time your call to setState()
has long finished.
The easiest way to see this is to add a few log statements to your code:
componentWillMount() {
this.userRef.on('value', snapshot => {
this.setState({
displayName: snapshot.val().displayName
});
});
this.usersItemsRef.on('value', snapshot => {
let usersItems = snapshot.val();
let newState = [];
console.log("Before attaching once listeners");
for (let userItem in usersItems) {
var itemRef = firebaseDB.database().ref(`/items/${userItem}`)
itemRef.once('value', snapshot => {
console.log("In once listener callback");
var item = snapshot.val()
newState.push({
id: itemRef.key,
title: item.title
});
})
}
console.log("After attaching once listeners");
this.setState({
items: newState
})
});
}
The output from this logging will be:
Before attaching once listeners
After attaching once listeners
In once listener callback
In once listener callback
...
This is probably not the order you expected. But it explains perfectly why your setState()
doesn't update the UI: the data hasn't been loaded yet.
The solution is to call setState()
when the data has been loaded. You do this by moving it **into* the callback:
componentWillMount() {
this.userRef.on('value', snapshot => {
this.setState({
displayName: snapshot.val().displayName
});
});
this.usersItemsRef.on('value', snapshot => {
let usersItems = snapshot.val();
let newState = [];
for (let userItem in usersItems) {
var itemRef = firebaseDB.database().ref(`/items/${userItem}`)
itemRef.once('value', snapshot => {
var item = snapshot.val()
newState.push({
id: itemRef.key,
title: item.title
});
this.setState({
items: newState
})
})
}
});
}
This will call setState()
for every item that is loaded. Usually React is pretty good with handling such incremental updates. But just in case it causes flicker, you can also wait for all items to be loaded by using Promise.all()
:
componentWillMount() {
this.userRef.on('value', snapshot => {
this.setState({
displayName: snapshot.val().displayName
});
});
this.usersItemsRef.on('value', snapshot => {
let usersItems = snapshot.val();
let newState = [];
let promises = [];
for (let userItem in usersItems) {
var itemRef = firebaseDB.database().ref(`/items/${userItem}`)
promises.push(itemRef.once('value'));
}
Promise.all(promises).then((snapshots) => {
snapshots.forEach((snapshot) => {
var item = snapshot.val()
newState.push({
id: itemRef.key,
title: item.title
});
});
this.setState({
items: newState
});
});
});
}
回答2:
I have not used FirebaseDB before but my guess is that it is asynchronous and returns a Promise
.
Therefore when use .then
or await
when calling the database to retrieve the reference.
来源:https://stackoverflow.com/questions/48655464/react-not-updating-render-after-setstate