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

React+TypeScript中Firebase/Firestore嵌套查询数据映射失败

React + Firestore 嵌套实时查询不显示子集合数据问题解决

问题描述

我尝试在React中渲染Firestore里的章节(sections)及其下属课时(lectures)数据,课时是嵌套在章节集合下的子集合。实现逻辑是先查询章节集合,再通过每个章节ID查询对应的课时集合。

控制台能看到章节和嵌套课时的完整数据,但网页上始终显示不了课时,只有保存代码改动时会短暂闪显0.1秒左右。推测是嵌套异步查询的重渲染问题,但对异步函数理解不够,找不到正确实现方式。

import React, {useState, useEffect} from 'react'
import { db } from '../../../firebase-config'
import {collection, onSnapshot, query} from 'firebase/firestore'

type CourseSections =  CourseSection[]

type CourseSection = {
  id: string,
  title: string,
  lectures: CourseLectures,
}

type CourseLectures = CourseLecture[]

type CourseLecture = {
  title: string,
  id: string,
}

const Test2:React.FC = () => {

const [data, setData] = useState<CourseSections>([])

useEffect(()=>{
   async function fetchData() {
      const q = query(collection(db, "products", "5yzLggfe5YC0PAM49enW", "sections"))
      onSnapshot(q, (snapshot)=> {
         let tempSections:CourseSections = []
         snapshot.docs.forEach((doc:any)=>{
            const w = query(collection(db, "products", "5yzLggfe5YC0PAM49enW", "sections", doc.id, "lectures"))
            let tempLectures:CourseLectures = [];
            onSnapshot(w, (snapshotB:any)=>{
               snapshotB.docs.forEach((doc:any)=>{
               tempLectures.push({...doc.data(), id: doc.id})
               })
            })
            tempSections.push({...doc.data(), id:  doc.id, lectures: tempLectures}) //
         })
         setData(tempSections)
      })
   }
   fetchData();
}, [])

return <>
   {data.map((section:CourseSection)=>{
      return <><h2>{section.title}</h2>
         {section.lectures.map((lecture:CourseLecture)=>{
            return <h4>{lecture.title}</h4>
         })}
      </>
   })}
</>
}
export default Test2

问题根源

  • 嵌套的onSnapshot是异步执行的,外层遍历章节时立刻将空的tempLectures塞入tempSections并调用setData,此时课时数据还未获取,页面渲染时lectures为空数组。
  • 课时快照回调触发时,tempLectures虽被填充,但tempSections已经用于更新state,后续修改无法触发组件重渲染。
  • 未清理onSnapshot的订阅,存在内存泄漏风险。

解决方案

方案1:一次性查询(非实时)

如果不需要实时监听数据变化,用getDocs替代onSnapshot,配合Promise.all等待所有课时数据加载完成后再更新state:

import React, { useState, useEffect } from 'react'
import { db } from '../../../firebase-config'
import { collection, query, getDocs } from 'firebase/firestore'

type CourseSections = CourseSection[]
type CourseSection = {
  id: string,
  title: string,
  lectures: CourseLectures,
}
type CourseLectures = CourseLecture[]
type CourseLecture = {
  title: string,
  id: string,
}

const Test2: React.FC = () => {
  const [data, setData] = useState<CourseSections>([])

  useEffect(() => {
    async function fetchData() {
      const sectionsQuery = query(collection(db, "products", "5yzLggfe5YC0PAM49enW", "sections"))
      const sectionsSnapshot = await getDocs(sectionsQuery)
      
      // 并行查询所有章节的课时数据
      const sectionsWithLectures = await Promise.all(
        sectionsSnapshot.docs.map(async (sectionDoc) => {
          const lecturesQuery = query(collection(db, "products", "5yzLggfe5YC0PAM49enW", "sections", sectionDoc.id, "lectures"))
          const lecturesSnapshot = await getDocs(lecturesQuery)
          const lectures = lecturesSnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })) as CourseLectures
          return { ...sectionDoc.data(), id: sectionDoc.id, lectures } as CourseSection
        })
      )
      
      setData(sectionsWithLectures)
    }
    fetchData()
  }, [])

  return (
    <>
      {data.map((section) => (
        <div key={section.id}>
          <h2>{section.title}</h2>
          {section.lectures.map((lecture) => (
            <h4 key={lecture.id}>{lecture.title}</h4>
          ))}
        </div>
      ))}
    </>
  )
}

export default Test2

方案2:实时订阅(支持数据实时更新)

如果需要实时监听章节和课时的变化,需正确管理每个课时的订阅,并在课时更新时单独更新对应章节的数据:

import React, { useState, useEffect } from 'react'
import { db } from '../../../firebase-config'
import { collection, query, onSnapshot, Unsubscribe } from 'firebase/firestore'

type CourseSections = CourseSection[]
type CourseSection = {
  id: string,
  title: string,
  lectures: CourseLectures,
}
type CourseLectures = CourseLecture[]
type CourseLecture = {
  title: string,
  id: string,
}

const Test2: React.FC = () => {
  const [data, setData] = useState<CourseSections>([])

  useEffect(() => {
    let unsubscribeSections: Unsubscribe | null = null
    const lectureUnsubscribers: Unsubscribe[] = []

    function setupSubscriptions() {
      const sectionsQuery = query(collection(db, "products", "5yzLggfe5YC0PAM49enW", "sections"))
      
      unsubscribeSections = onSnapshot(sectionsQuery, (sectionsSnapshot) => {
        // 清理之前的课时订阅,避免内存泄漏
        lectureUnsubscribers.forEach(unsub => unsub())
        lectureUnsubscribers.length = 0

        const tempSections: CourseSections = []
        
        sectionsSnapshot.docs.forEach((sectionDoc) => {
          const sectionData = { ...sectionDoc.data(), id: sectionDoc.id, lectures: [] } as CourseSection
          tempSections.push(sectionData)

          // 为当前章节设置课时实时订阅
          const lecturesQuery = query(collection(db, "products", "5yzLggfe5YC0PAM49enW", "sections", sectionDoc.id, "lectures"))
          const unsubscribeLectures = onSnapshot(lecturesQuery, (lecturesSnapshot) => {
            const lectures = lecturesSnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })) as CourseLectures
            // 函数式更新state,确保基于最新数据修改
            setData(prev => prev.map(sec => sec.id === sectionDoc.id ? { ...sec, lectures } : sec))
          })
          
          lectureUnsubscribers.push(unsubscribeLectures)
        })
        
        // 初始化章节数据
        setData(tempSections)
      })
    }

    setupSubscriptions()

    // 组件卸载时清理所有订阅
    return () => {
      if (unsubscribeSections) unsubscribeSections()
      lectureUnsubscribers.forEach(unsub => unsub())
    }
  }, [])

  return (
    <>
      {data.map((section) => (
        <div key={section.id}>
          <h2>{section.title}</h2>
          {section.lectures.map((lecture) => (
            <h4 key={lecture.id}>{lecture.title}</h4>
          ))}
        </div>
      ))}
    </>
  )
}

export default Test2

关键改进点

  • 异步处理:方案1用Promise.all确保所有数据加载完成后再渲染;方案2在课时快照回调中单独更新对应章节,保证实时性。
  • 订阅清理:在useEffect返回函数中取消所有onSnapshot订阅,避免内存泄漏。
  • key属性:为渲染元素添加唯一key,帮助React正确识别元素变化,优化重渲染。
  • 状态更新:使用函数式更新setData(prev => ...),确保基于最新state修改,避免状态覆盖。

内容的提问来源于stack exchange,提问作者CurlyHead

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 22:55:23