Load Firestore Data in Component Before Render Using Vuefire

495 Views Asked by At

I am trying to populate a table of people with their name and a profile picture. The name is sourced from a Firestore database, and the picture uses the name to find a related picture in a Firebase Storage bucket.

I have watched hours of videos and have scoured nearly a hundred articles at this point, so my example code has pieces from each as I've been trying every combination and getting mixed but unsuccessful results.

In this current state which returns the least amount of errors, I am able to successfully populate the table with the names, however in that same component it is not able to pull the profile picture. The value used for the profile picture is updated, but it is updated from the placeholder value to undefined.

GamePlanner.vue

<template>
    <div>
    <Field />
    <Bench />
    <!-- <Suspense>
        <template #default> -->
            <PlanSummary />
        <!-- </template>
        <template #fallback>
            <div class="loading">Loading...</div>
        </template>
    </Suspense> -->
    </div>
</template>

PlanSummary.vue

<template>
    <div class="summaryContainer">
        <div class="inningTableContainer">
            <table class="inningTable">
                <tbody>
                    <!-- <Suspense> -->
                        <!-- <template #default> -->
                            <PlayerRow v-for="player in players.value" :key="player.id" :player="(player as Player)" />
                        <!-- </template> -->

                        <!-- <template #fallback>
                            <tr data-playerid="0">
                                <img class="playerImage" src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50" />
                                Loading players...
                                <span class="playerNumber">00</span>
                            </tr>
                        </template> -->
                    <!-- </Suspense> -->
                </tbody>
            </table>
        </div>

    </div>
</template>

<script setup lang="ts">
import { computed, onErrorCaptured, ref } from "vue";
import { useFirestore, useCollection } from "vuefire";
import { collection } from "firebase/firestore";
import { Player, Inning } from "@/definitions/GamePlanner";
import PlayerRow from "./PlayerRow.vue";

const db = useFirestore();
const gamePlanID = "O278vlB9Xx39vkZvIsdP";

// const players = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));

// const players = ref(useCollection(collection(db, `/gameplans/${gamePlanID}/participants`)));

// Unhandled error during execution of scheduler flush
// Uncaught (in promise) DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
const players = ref(); 
players.value = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));

// Seeminly infinite loop with "onServerPrefetch is called when there is no active component instance to be associated with."
// Renders 5 (??) undefined players
// One error shown: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'substring')
// const players = computed(() => useCollection(collection(db, `/gameplans/${gamePlanID}/participants`)));

// onErrorCaptured((error, vm, info) => {
//     console.log("Error loading Summary component: ", error, "vm: ", vm, "info: ", info);
//     throw error;
// });
</script>

PlayerRow.vue

<template>
    <tr :key="player2.id" :data-playerid="player2.id">
        <td>
            <img class="playerImage" :src="playerPictureURL" />
            {{ player2.nickname || player2.firstName + " " + player2.lastName }}
            <span class="playerNumber">{{ player2.playerNumber }}</span>
        </td>
    </tr>
</template>

<script lang="ts" setup>
import { ref, PropType, computed, onMounted, watch } from "vue";
import { useFirebaseStorage, useStorageFileUrl } from "vuefire";
import { ref as storageRef } from 'firebase/storage';
import { Player, Inning } from "@/definitions/GamePlanner";

const fs = useFirebaseStorage();
const props = defineProps({
    'player': { type: Object as PropType<Player>, required: true },
    // 'innings': Array<Inning>
});
const player2 = ref(props.player);

// const innings = computed(() => props.innings);

// const playerPictureURL = computed(() => {
//     const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
//     const playerPictureResource = storageRef(fs, `playerPictures/${playerPictureFilename}`);
//     return useStorageFileUrl(playerPictureResource).url.value as string;
// });
// const playerPictureURL = ref(() => {
//     const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
//     const playerPictureResource = storageRef(fs, `playerPictures/${playerPictureFilename}`);
//     return useStorageFileUrl(playerPictureResource).url.value as string;
// });

const playerPictureURL = ref("https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50");
async function getPlayerPictureURL() {
    console.log("PlayerRow.ts getPlayerPictureURL");
    const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
    const playerPictureResource = await storageRef(fs, `playerPictures/${playerPictureFilename}`);
    playerPictureURL.value = await useStorageFileUrl(playerPictureResource).url.value as string;
}
onMounted(() => {
    console.log("PlayerRow.ts onMounted");
    getPlayerPictureURL();
});
watch(playerPictureURL, (newVal, oldVal) => {
    console.log("PlayerRow.ts watch playerPictureURL");
    console.log("newVal: " + newVal);
    console.log("oldVal: " + oldVal);
});
</script>

I was under the impression that <Suspense> would need to wrap the <PlayerRow> component since I am using the storageRef and useStorageUrl methods, but it seems to introduce more issues. Based on the vuefire documentation and inspecting the definitions int he code itself, it does not appear that they are asynchronous, however trying to to immediately invoke them does not produce an immediate/actual result.

Relevant Package Versions

{
    "vue": "^3.2.45"
    "firebase": "^9.15.0",
    "typescript": "^4.9.3",
    "vite": "^4.0.0",
    "vue-router": "^4.1.6",
    "vue-tsc": "^1.0.11",
    "vuefire": "3.0.0-beta.6"
}
1

There are 1 best solutions below

0
On

According to this documentation we are supposed to use useCollection with including collection as a argument as follows:

const todos = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`))

And I see you are using it the correct way but instead of assigning to a ref you can assign it directly to players variable. As you are trying to use this reactive object as the value of a ref object, which is not supported.

You can solve your issue with changing this line:

const players = ref(); 
players.value = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));

To :

const players = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));

For more information about this topic you can go through the following docs