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

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类型不匹配错误。

修复步骤

  1. 直接通过useDocument/useCollection创建响应式引用,利用其支持动态切换Firestore引用的特性
  2. 监听userId变化,动态更新对应的Firestore文档/集合引用
  3. 模板中通过.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 06:07:09