【问题标题】:I need to pull data from another Firestore collection based on forEach data within a onSnapshot call我需要根据 onSnapshot 调用中的 forEach 数据从另一个 Firestore 集合中提取数据
【发布时间】:2022-01-13 21:25:15
【问题描述】:

我该怎么做?我无法从我在这里找到的其他任何东西中弄清楚。

背景

我在 Firestore 中有 postsusers 的集合。信息将被渲染到一个帖子组件中,在仪表板上显示所有现有帖子。

用户收藏

users 拥有一个存储图像 URL 的 avatar 属性。每个用户的 doc id 只是他们的用户名,因为它们是唯一的。

帖子收藏

posts 有一个与username/doc.id 完全相同的author 属性

目标

在遍历posts 时,我想将它们推送到一个数组并存储它们的id 和其余的post 数据。我还需要将其与users 集合相关联,并在每次迭代中找到与post author 匹配的useravatar

我已经尝试过的事情

我尝试在forEach 循环中使用async/await,使用post.author 值来获取正确的user document 并拉动avatar

Posts组件代码

import { useEffect, useState } from "react"
import { Link } from "react-router-dom"
import { collection, onSnapshot /*doc, getDoc*/ } from "firebase/firestore"
import { db } from "lib/firebase"
import AllPostsSkeleton from "components/Skeletons/AllPostsSkeleton"
import Tags from "components/Tags"
import defaultAvatar from "assets/images/avatar_placeholder.png"

const Posts = () => {
  const [loading, setLoading] = useState(true)
  const [posts, setPosts] = useState(null)

  useEffect(() => {
    const unsubscribe = onSnapshot(
      collection(db, "posts"),
      (docs) => {
        let postsArray = []

        docs.forEach((post) => {
          // const docRef = doc(db, "users", post.author)
          // const docSnap = await getDoc(docRef)

          postsArray.push({
            id: post.id,
            // avatar: docSnap.data().avatar,
            ...post.data(),
          })
        })

        setPosts(postsArray)
      },
      (error) => {
        console.log(error)
      }
    )

    setLoading(false)

    return () => unsubscribe()
  }, [])

  if (loading) return <AllPostsSkeleton />

  if (!posts) return <div className="no-posts">No posts to show</div>

  const RenderPosts = () => {
    const sortedPosts = posts.sort((a, b) => {
      return new Date(b.date.seconds) - new Date(a.date.seconds)
    })

    return sortedPosts.map(
      ({ id, author, slug, content, tags, comment_count, avatar }) => (
        <article className="post-preview media" key={id}>
          <figure className="media-left">
            <p className="comment-avatar image is-96x96">
              <img src={avatar || defaultAvatar} alt={content.title} />
            </p>
          </figure>
          <div className="media-content">
            <div className="content">
              <header className="post-header">
                <h2 className="title is-3">
                  <Link to={`/user/${author}/posts/${slug}`}>
                    {content.title}
                  </Link>
                </h2>
                <div className="tags">
                  <Tags data={tags} />
                </div>
              </header>
              <p className="post-excerpt">{content.excerpt}</p>
              <footer className="post-footer">
                Posted by
                <Link to={`/user/${author}`} className="capitalise">
                  {author}
                </Link>
                | <Link to={`/user/${author}/posts/${slug}`}>View Post</Link>
              </footer>
              <div className="content">
                <Link to={`/user/${author}/posts/${slug}#comments`}>
                  Comments ({comment_count})
                </Link>
              </div>
            </div>
          </div>
        </article>
      )
    )
  }

  return (
    <div className="posts-list">
      <RenderPosts />
    </div>
  )
}

export default Posts

【问题讨论】:

    标签: javascript reactjs firebase google-cloud-firestore


    【解决方案1】:

    您面临的问题是因为您在 forEach 循环中运行 await。 For Each 循环不返回任何内容,因此您无法在其中运行 await 。请更改 forEach 以映射您的逻辑应该可以正常工作。

    【讨论】:

      【解决方案2】:

      在使用 Promise 时,尤其是使用 Firebase SDK,您会看到经常使用 Promise.all(docs.map((doc) =&gt; Promise&lt;Result&gt;)),以便 Promise 正确链接在一起。

      即使正确链接 Promise,也会引入一个新问题,即如果在处理文档的前一个快照时收到快照,您现在将有两组数据相互争用调用 setPosts。为了解决这个问题,每次快照监听器再次触发时,您都应该“取消”由上一次执行监听器触发的 Promise 链。

      function getAvatarsForEachPost(postDocSnaps) {
        return Promise.all(
          postDocSnaps.map((postDocSnap) => {
            const postData = postDocSnap.data();
      
            const userDocRef = doc(db, "users", postData.author)
            const userDocSnap = await getDoc(userDocRef)
      
            return {
              id: postDocSnap.id,
              avatar: userDocSnap.get("avatar"),
              ...postData,
            };
          })
        )
      }
      
      useEffect(() => {
        let cancelPreviousPromiseChain = undefined;
        const unsubscribe = onSnapshot(
          collection(db, "posts"),
          (querySnapshot) => { // using docs here is a bad practice as this is a QuerySnapshot object, not an array of documents
            if (cancelPreviousPromiseChain) cancelPreviousPromiseChain(); // cancel previous run if possible
      
            let cancelled = false;
            cancelPreviousPromiseChain = () => cancelled = true;
      
            getAvatarsForEachPost(querySnapshot.docs)
              .then((postsArray) => {
                if (cancelled) return; // cancelled, do nothing.
                setLoading(false)
                setPosts(postsArray)
              })
              .catch((error) => {
                if (cancelled) return; // cancelled, do nothing.
                setLoading(false)
                console.log(error)
              })
          },
          (error) => {
            if (cancelPreviousChain) cancelPreviousChain(); // now the listener has errored, cancel using any stale data
            setLoading(false)
            console.log(error)
          }
        )
      
        return () => {
          unsubscribe(); // detaches the listener
          if (cancelPreviousChain) cancelPreviousChain(); // prevents any outstanding Promise chains from calling setPosts/setLoading
        }
      }, [])
      

      注意事项:

      • setLoading(false) 应该只在你有数据后调用,而不是像你原来的答案那样在你附加监听器后立即调用。
      • 考虑使用 setError() 或类似名称,这样您就可以为您的用户呈现一条消息,让他们知道出了什么问题。
      • 您可以让子 &lt;Post&gt; 组件处理获取头像 URL 以简化上述代码。
      • 考虑创建一个 getUserAvatar(uid) 函数,该函数具有 uid‑&gt;Promise&lt;AvatarURL&gt; 条目的内部缓存映射,这样对同一头像的多次调用就不会向数据库发出大量请求以获取相同的信息。

      【讨论】:

      • 哦,伙计,多么令人难以置信的答案,非常感谢。这么多信息,我真的很感激。我什至没有考虑过使用子 Post 组件。我对自己的项目有点混乱,因为我不知道当我开始时我要做什么,所以我没有计划好的组件,最终把所有东西都拼凑在一起。我需要在它变得更加混乱之前进行重构。船上的笔记谢谢!
      猜你喜欢
      • 2019-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-23
      • 2023-03-05
      • 2016-07-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多