在 SetState 之后 React 不更新渲染

IT技术 javascript reactjs firebase firebase-realtime-database
2021-05-15 18:50:06

我已经玩了一段时间的代码了。我正在从 Firebase 获取数据并将该数据中的对象列表填充到此 UserProfile 页面上。我只是想让它工作,这样当我进入个人资料页面时,那里会列出他们的项目列表。

问题是,使用此版本的代码,我必须单击配置文件链接两次才能显示项目,但显示名称显示正常。我知道 setState 是异步的。我试过在 setState 的回调中设置状态。我试过事先检查快照是否存在。我试过 componentDidMount 和 componentDidUpdate。这些事情都没有帮助,我只是最终无法调用 this.state.items.map 或者 newState 是一些空的、奇怪的东西。我现在不知道我哪里错了。

当我控制台调试这个时,看起来 setState 正在被调用,因为到目前为止没有从 Firebase 获取任何东西并且以后永远不会被调用。

也许我设置 newState 的方式有问题,因为当我尝试第一次设置时控制台日志 newState 为空。我只是不知道何时在第一次渲染的适当时间设置它。

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>
      );
  }
}
2个回答

数据是从 Firebase 异步加载的,因为它可能需要不确定的时间。当数据可用时,Firebase 调用您传入的回调函数。但到那时您的调用setState()已经结束。

查看这一点的最简单方法是在您的代码中添加一些日志语句:

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

此日志记录的输出将是:

在附加一次听众之前

附加一次听众后

在一次侦听器回调中

在一次侦听器回调中

...

这可能不是您期望的顺序。但这完美地解释了为什么您setState()不更新 UI:数据尚未加载。

解决方法是setState()在数据加载完毕后调用您可以通过将其 ** 移入 * 回调来完成此操作:

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

这将调用setState()加载的每个项目。通常 React 非常擅长处理此类增量更新。但为了防止它导致闪烁,您也可以使用以下命令等待所有项目加载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
      });
    });
  });
}

我以前没有使用过 FirebaseDB,但我猜它是异步的,并返回一个Promise. 因此在使用.thenawait调用数据库检索引用时。