简体   繁体   English

我需要根据 onSnapshot 调用中的 forEach 数据从另一个 Firestore 集合中提取数据

[英]I need to pull data from another Firestore collection based on forEach data within a onSnapshot call

How do I go about this?我该如何 go 关于这个? I can't figure it out from anything else I can find on here.我无法从我在这里找到的任何其他东西中弄清楚。

Background背景

I have collections in the Firestore for posts and users .我在 Firestore 中有 collections 用于postsusers The information is going to be rendered out in to a Posts components displaying all of the existing posts on the dashboard.该信息将被呈现到一个帖子组件中,该组件显示仪表板上的所有现有帖子。

Users Collection用户集合

users holds an avatar property which stores an image URL. users拥有一个avatar属性,该属性存储图像 URL。 The doc id for each user is just their username as these are unique.每个用户的 doc id 只是他们的用户名,因为它们是唯一的。

Posts Collection帖子集合

posts have an author property which is exactly the same as the username / doc.id posts具有与username / doc.id完全相同的author属性

The Aim目的

When iterating through the posts I want to push them to an array and store their id and the rest of the post data.在遍历posts时,我想将它们推送到一个数组并存储它们的idpost数据的 rest。 I also need to relate this to the users collection and with each iteration, find the avatar of the user that matches the post author .我还需要将其与users集合相关联,并在每次迭代中找到与post author匹配的user avatar

Things I've already tried我已经尝试过的事情

I have tried using async/await within the forEach loop, using the post.author value to get the correct user document and pulling the avatar .我尝试在forEach循环中使用async/await ,使用post.author值来获取正确的user document并拉动avatar

Posts component code 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

The issue you are facing is because you are running an await inside a forEach loop.您面临的问题是因为您在 forEach 循环中运行 await 。 For Each loop doesn't return anything so you cant run await inside it. For Each 循环不返回任何内容,因此您无法在其中运行 await 。 Please change the forEach to Map your logic should be working fine.请将 forEach 更改为 Map 您的逻辑应该可以正常工作。

When working with Promises, especially with the Firebase SDK, you will see the Promise.all(docs.map((doc) => Promise<Result>)) being used often so that the Promises are correctly chained together.使用 Promise 时,特别是使用 Firebase SDK 时,您会看到Promise.all(docs.map((doc) => Promise<Result>))经常被正确使用。

Even if you chain the Promises properly, you introduce a new issue where if a snapshot is received while you are still processing the previous snapshot of documents, you will now have two sets of data fighting each other to call setPosts .即使您正确地链接 Promises,您也会引入一个新问题,如果在您仍在处理文档的先前快照时收到快照,您现在将有两组数据相互竞争以调用setPosts To solve this, each time the snapshot listener fires again, you should "cancel" the Promise chain fired off by the previous execution of the listener.为了解决这个问题,每次快照侦听器再次触发时,您都应该“取消”之前执行侦听器触发的 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
  }
}, [])

Notes:笔记:

  • setLoading(false) should only be called once you have data, not as soon as you attach the listener as in your original answer. setLoading(false)只应在您拥有数据后调用,而不是在您像原始答案中那样附加侦听器后立即调用。
  • Consider using setError() or similar so you can render a message for your user so they know something went wrong.考虑使用setError()或类似的方法,这样您就可以为您的用户呈现一条消息,以便他们知道出了什么问题。
  • You could let a child <Post> component handle fetching the avatar URL instead to simplify the above code.您可以让子<Post>组件处理获取头像 URL 来简化上述代码。
  • Consider making a getUserAvatar(uid) function that has an internal cached map of uid‑>Promise<AvatarURL> entries so that many calls for the same avatar don't make lots of requests to the database for the same information.考虑创建一个getUserAvatar(uid) function,它有一个内部缓存的 map 的uid‑>Promise<AvatarURL>条目,这样对同一头像的许多调用就不会向数据库发出大量请求以获取相同的信息。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM