[英]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.我无法从我在这里找到的任何其他东西中弄清楚。
I have collections in the Firestore for posts
and users
.我在 Firestore 中有 collections 用于posts
和users
。 The information is going to be rendered out in to a Posts components displaying all of the existing posts on the dashboard.该信息将被呈现到一个帖子组件中,该组件显示仪表板上的所有现有帖子。
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
have an author
property which is exactly the same as the username
/ doc.id
posts
具有与username
/ doc.id
完全相同的author
属性
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
时,我想将它们推送到一个数组并存储它们的id
和post
数据的 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
。
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)
只应在您拥有数据后调用,而不是在您像原始答案中那样附加侦听器后立即调用。setError()
or similar so you can render a message for your user so they know something went wrong.考虑使用setError()
或类似的方法,这样您就可以为您的用户呈现一条消息,以便他们知道出了什么问题。<Post>
component handle fetching the avatar URL instead to simplify the above code.您可以让子<Post>
组件处理获取头像 URL 来简化上述代码。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.