You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+TypeScript+Firebase V9如何获取指定子集合数据?

React+TypeScript Firebase评论功能修复方案

问题根源

  1. 状态类型错误:info是存储所有帖子的数组状态,你直接访问info.id,但数组本身没有id属性,必然抛出Cannot read properties of undefined (reading 'id')错误。
  2. 评论状态设计不合理:全局的comments数组会让所有帖子共用同一批评论,无法区分不同帖子的评论内容。
  3. 获取评论逻辑错误:全局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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 14:40:34