React+TypeScript+Firebase V9如何获取指定子集合数据?
React+TypeScript Firebase评论功能修复方案
问题根源
- 状态类型错误:
info是存储所有帖子的数组状态,你直接访问info.id,但数组本身没有id属性,必然抛出Cannot read properties of undefined (reading 'id')错误。 - 评论状态设计不合理:全局的
comments数组会让所有帖子共用同一批评论,无法区分不同帖子的评论内容。 - 获取评论逻辑错误:全局
useEffect无法针对每个帖子单独拉取评论,且没有依赖触发更新。
修复步骤
1. 调整状态结构
将评论状态改为对象,以帖子ID为键存储对应评论列表,避免全局共享;同时给数据加上TypeScript类型定义,减少any的使用:
interface Comment { id: string; comment: string; username: string | null; createdAt: firebase.firestore.Timestamp; } interface Post { id: string; writer: string; title: string; content: string; createdAt: firebase.firestore.Timestamp; } const [info, setInfo] = useState<Post[]>([]); const [commentsByPostId, setCommentsByPostId] = useState<Record<string, Comment[]>>({});
2. 移除全局评论useEffect,改用帖子级别的评论获取
创建单独的评论拉取函数,在渲染每个帖子时调用:
const fetchComments = (postId: string) => { const ref = query( collection(db, "Info", postId, "comments"), orderBy("createdAt", "desc") ); return onSnapshot(ref, (snapshot) => { const postComments: Comment[] = snapshot.docs.map(doc => ({ ...doc.data() as Omit<Comment, 'id'>, id: doc.id })); setCommentsByPostId(prev => ({ ...prev, [postId]: postComments })); }); };
在渲染每个帖子时,通过useEffect挂载拉取评论的监听:
{info.map((post) => ( <Paper key={post.id}> {/* 帖子内容渲染 */} {/* ... */} {/* 评论列表渲染 */} {(commentsByPostId[post.id] || []).map((comment) => ( <Grid container key={comment.id}> <Grid item xs={8}> <Typography variant="h6">{comment.comment}</Typography> </Grid> </Grid> ))} {/* 评论提交表单 */} {/* ... */} {/* 挂载时拉取当前帖子的评论 */} {useEffect(() => { const unsub = fetchComments(post.id); return () => unsub(); }, [post.id])} </Paper> ))}
3. 修正评论提交逻辑
确保提交评论时关联当前帖子的ID,同时增加空值判断:
<form onSubmit={(e) => { e.preventDefault(); if (!user?.displayName || !comment.trim()) return; const ref = collection(db, "Info", post.id, "comments"); addDoc(ref, { comment: comment.trim(), username: user.displayName, createdAt: serverTimestamp(), }); setComment(""); }} >
完整修复代码
import { useState, useEffect } from 'react'; import { Container, Paper, Grid, Typography, TextField, Button } from '@mui/material'; import { FavoriteBorderOutlinedIcon, ChatOutlinedIcon } from '@mui/icons-material'; import { collection, query, orderBy, onSnapshot, addDoc, serverTimestamp } from 'firebase/firestore'; import { db } from './firebaseConfig'; // 你的Firebase配置文件 import { useAuthContext } from './AuthContext'; // 你的Auth上下文 interface Comment { id: string; comment: string; username: string | null; createdAt: firebase.firestore.Timestamp; } interface Post { id: string; writer: string; title: string; content: string; createdAt: firebase.firestore.Timestamp; } export const Info = () => { const [info, setInfo] = useState<Post[]>([]); const [commentsByPostId, setCommentsByPostId] = useState<Record<string, Comment[]>>({}); const [comment, setComment] = useState(""); const { user } = useAuthContext(); useEffect(() => { const ref = query(collection(db, "Info"), orderBy("createdAt", "desc")); const unsub = onSnapshot(ref, (snapshot) => { const posts: Post[] = snapshot.docs.map(doc => ({ ...doc.data() as Omit<Post, 'id'>, id: doc.id })); setInfo(posts); }); return () => unsub(); }, []); const fetchComments = (postId: string) => { const ref = query( collection(db, "Info", postId, "comments"), orderBy("createdAt", "desc") ); return onSnapshot(ref, (snapshot) => { const postComments: Comment[] = snapshot.docs.map(doc => ({ ...doc.data() as Omit<Comment, 'id'>, id: doc.id })); setCommentsByPostId(prev => ({ ...prev, [postId]: postComments })); }); }; return ( <Container> {info.map((post) => ( <Paper key={post.id}> <Grid container id={post.id} marginTop={2} marginBottom={2} alignItems="center" > <Grid item xs={4} justifyContent="start"> <Typography variant="h6">{post.writer}</Typography> </Grid> <Paper style={{ width: "100%", backgroundColor: "#652d90" }}> <Grid item md={12} xs={12}> <Typography variant="h6" className="goal" style={{ color: "white" }} > {post.title} </Typography> </Grid> <Grid item md={12} xs={12}> <Typography variant="subtitle1" className="goal" style={{ color: "white" }} > {post.content} </Typography> </Grid> </Paper> <Grid container> <Grid item xs={1}> <FavoriteBorderOutlinedIcon /> </Grid> </Grid> {/* 评论列表 */} {(commentsByPostId[post.id] || []).map((comment) => ( <Grid container key={comment.id}> <Grid item xs={8}> <Typography variant="h6">{comment.comment}</Typography> </Grid> </Grid> ))} {/* 评论提交表单 */} <Grid container marginTop={1}> <Grid item xs={12}> <form onSubmit={(e) => { e.preventDefault(); if (!user?.displayName || !comment.trim()) return; const ref = collection(db, "Info", post.id, "comments"); addDoc(ref, { comment: comment.trim(), username: user.displayName, createdAt: serverTimestamp(), }); setComment(""); }} > <Grid container alignItems="center"> <Grid item xs={2}> <ChatOutlinedIcon /> </Grid> <Grid item xs={7}> <TextField label="Add a comment..." color="primary" variant="standard" fullWidth value={comment} onChange={(e) => setComment(e.target.value)} /> </Grid> <Grid item xs={1}> <Button type="submit" disabled={!comment.trim()}>Post</Button> </Grid> </Grid> </form> </Grid> </Grid> </Grid> {/* 拉取当前帖子的评论 */} {useEffect(() => { const unsub = fetchComments(post.id); return () => unsub(); }, [post.id])} </Paper> ))} </Container> ); };
内容的提问来源于stack exchange,提问作者JosineD58
相关产品推荐
相关产品推荐

