Vue3+VueFire中useCollection集合不更新及类型错误求助
问题
在Vue3 + VueFire项目中尝试展示场景列表,网络请求已完成但数据未同步,添加用户名支持后页面始终显示“No scenes created yet”。核心问题是执行scenesCollection.value = useCollection(scenesCollectionRef);后,集合始终为空数组。
TypeScript报错信息:
Type '_RefFirestore<UserInformation | undefined>' is not assignable to type 'null'. ts(2322) Type '_RefFirestore<Scene[]>' is missing the following properties from type 'never[]': length, pop, push, concat, and 35 more. ts(2740)
手动指定ref类型无效,修复类型错误后功能仍异常。
组件完整代码
模板部分
<template> <div class="wrapper"> <div v-if="!userDocument"> <p>This user does not exist</p> </div> <div v-else-if="!scenesCollection"> <p>This user does have public scenes</p> </div> <v-container v-else-if="scenesCollection.length" fluid> <v-row dense> <v-col v-for="scene in scenesCollection" :key="scene.id"> <router-link :to="'/'+ userId + '/' + scene.id" class="clickable"> <v-card> <v-img :src="scene.coverUrl" class="align-end" gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)" height="200px" cover > <v-card-title class="text-white">{{ scene.friendlyName }}</v-card-title> </v-img> </v-card> </router-link> </v-col> </v-row> </v-container> <div v-else class="clickable"> <br onload="alert('banana')" /> <p>No scenes created yet</p> </div> </div> </template>
脚本部分
import { ref, watchEffect } from 'vue'; import { useFirestore, useCurrentUser, useCollection, useDocument, } from 'vuefire'; import { DocumentReference, collection, doc, getDoc } from 'firebase/firestore'; import { useRoute } from 'vue-router'; import type { Scene, UserInformation, UsernameInformation } from '@/types'; const db = useFirestore(); const route = useRoute(); const currentUser = useCurrentUser(); const username = route.params.username; const isOnMePage = username === 'me'; const userId = ref<string | null>(null); const fetchUserIdFromUsername = async (username: string) => { const docRef = doc( db, 'usernames', username, ) as DocumentReference<UsernameInformation>; try { const usernameInfo = await getDoc(docRef); if (!usernameInfo.exists()) return ''; return usernameInfo.data().uid; } catch (error) { console.error('Error fetching user ID:', error); return ''; } }; watchEffect(async () => { // Logic for determining userId if (isOnMePage && currentUser.value) { userId.value = currentUser.value.uid; } else if (typeof username === 'string') { userId.value = await fetchUserIdFromUsername(username); } }); let userDocument = ref(null); let scenesCollection = ref([]); watchEffect(() => { console.log('Watching effect', { userId: userId.value }); if (!userId.value) { userDocument.value = null; scenesCollection.value = []; return; } const userDocumentRef = doc( db, 'users', userId.value, ).withConverter<UserInformation>({ fromFirestore: (snapshot) => { // this avoids having `null` while the server timestamp is updating const data = snapshot.data({ serverTimestamps: 'estimate', }) as UserInformation; console.log('New userInfo from firestore', { ...data, id: snapshot.id }); return { ...data, id: snapshot.id }; }, toFirestore: (data: UserInformation) => data, }); const scenesCollectionRef = collection( db, 'users', userId.value, 'scenes', ).withConverter<Scene>({ fromFirestore: (snapshot) => { const data = snapshot.data({ serverTimestamps: 'estimate' }) as Scene; console.log('New scene from firestore', { ...data, id: snapshot.id }); return { ...data, id: snapshot.id }; }, toFirestore: (data: Scene) => data, }); userDocument.value = useDocument(userDocumentRef); scenesCollection.value = useCollection(scenesCollectionRef); });
解决方法
核心问题分析
VueFire的useDocument和useCollection返回的是Firestore响应式引用对象,而非直接的文档/集合数据。直接将该对象赋值给普通Vue ref,会导致模板无法正确读取内部数据,同时触发TypeScript类型不匹配错误。
修复步骤
- 直接通过
useDocument/useCollection创建响应式引用,利用其支持动态切换Firestore引用的特性 - 监听
userId变化,动态更新对应的Firestore文档/集合引用 - 模板中通过
.value访问Firestore引用的实际数据
修改后的完整代码
脚本部分
import { ref, watchEffect, watch } from 'vue'; import { useFirestore, useCurrentUser, useCollection, useDocument, } from 'vuefire'; import { collection, doc, getDoc } from 'firebase/firestore'; import { useRoute } from 'vue-router'; import type { Scene, UserInformation, UsernameInformation } from '@/types'; const db = useFirestore(); const route = useRoute(); const currentUser = useCurrentUser(); const username = route.params.username; const isOnMePage = username === 'me'; const userId = ref<string | null>(null); // 存储动态Firestore引用的Vue ref const userDocRef = ref<ReturnType<typeof doc> | null>(null); const scenesColRef = ref<ReturnType<typeof collection> | null>(null); // 使用VueFire API创建响应式引用,传入返回动态引用的函数 const userDocument = useDocument(() => userDocRef.value); const scenesCollection = useCollection(() => scenesColRef.value); const fetchUserIdFromUsername = async (username: string) => { const docRef = doc(db, 'usernames', username) as typeof doc<UsernameInformation>; try { const usernameInfo = await getDoc(docRef); if (!usernameInfo.exists()) return null; return usernameInfo.data().uid; } catch (error) { console.error('Error fetching user ID:', error); return null; } }; // 处理userId的获取逻辑 watchEffect(async () => { if (isOnMePage && currentUser.value) { userId.value = currentUser.value.uid; } else if (typeof username === 'string') { userId.value = await fetchUserIdFromUsername(username); } else { userId.value = null; } }); // 监听userId变化,更新Firestore引用 watch(userId, (newUserId) => { if (!newUserId) { userDocRef.value = null; scenesColRef.value = null; return; } // 更新用户文档引用并附加转换器 userDocRef.value = doc(db, 'users', newUserId).withConverter<UserInformation>({ fromFirestore: (snapshot) => { const data = snapshot.data({ serverTimestamps: 'estimate' }) as UserInformation; return { ...data, id: snapshot.id }; }, toFirestore: (data: UserInformation) => data, }); // 更新场景集合引用并附加转换器 scenesColRef.value = collection(db, 'users', newUserId, 'scenes').withConverter<Scene>({ fromFirestore: (snapshot) => { const data = snapshot.data({ serverTimestamps: 'estimate' }) as Scene; return { ...data, id: snapshot.id }; }, toFirestore: (data: Scene) => data, }); });
模板部分(调整数据访问方式)
<template> <div class="wrapper"> <div v-if="!userDocument.value"> <p>This user does not exist</p> </div> <div v-else-if="!scenesCollection.value"> <p>This user does have public scenes</p> </div> <v-container v-else-if="scenesCollection.value.length" fluid> <v-row dense> <v-col v-for="scene in scenesCollection.value" :key="scene.id"> <router-link :to="'/'+ userId + '/' + scene.id" class="clickable"> <v-card> <v-img :src="scene.coverUrl" class="align-end" gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)" height="200px" cover > <v-card-title class="text-white">{{ scene.friendlyName }}</v-card-title> </v-img> </v-card> </router-link> </v-col> </v-row> </v-container> <div v-else class="clickable"> <p>No scenes created yet</p> </div> </div> </template>
关键说明
- VueFire的
useDocument/useCollection接受返回Firestore引用的函数,当引用变化时会自动重新订阅数据,保留实时更新特性 - 模板中必须通过
.value访问Firestore引用的实际数据,因为userDocument和scenesCollection本身是Vue ref,内部包裹的是Firestore的响应式数据对象 - 类型错误会自动解决,VueFire会根据传入的转换器推断正确类型
内容的提问来源于stack exchange,提问作者cadesalaberry
相关产品推荐
相关产品推荐

