Reload favorites

Hello

I am building an online recipe book using ionic react and firebase as backend. A user has to be able to add a recipe to his favorites.

In firebase i have a collecion ‘users’. Every user has an array field ‘favoriterecipes’ containing the id’s of all the recipes he/she has favorited.
I have an interface Auth wich describes the user collection in firebase. Whenever a user is logged in, his data is added to a global state.

When i press the ‘favorite’ button on a recipe, the id of the recipe gets added to the favorite recipes array in firebase. Then when i go back to the page that shows the user all his recipes that are favorited, i first have to reload the page before it adds the newly favorited recipe. I think it is cause the function read(id) uses .get() and i should use .onSnapshot to add a listener. I added the function read2(id) to try this out. When i use read2 it gives an error ‘: Property ‘username’ does not exist on type ‘void’.’
(same for favoriterecipes and badges) in the useAuthInit function under 'setAuthInit({…}

Is this indeed the problem? if yes: anyone knows how i can fix the error? If not: anyone knows how to fix this?

Code for auth:

export function useAuthInit(): AuthInit {
    const [authInit, setAuthInit] = useState<AuthInit>({loading: true});
    useEffect(() => {
        return firebaseAuth.onAuthStateChanged((firebaseUser) => {
            if(firebaseUser){
                read2(firebaseUser.uid).then((user) => {
                    console.log("user: ",user);
                    setAuthInit({
                        loading: false,
                        auth:  {
                            loggedIn: true,
                            userId: firebaseUser.uid,
                            userName: user.username,
                            favoriteRecipes: user.favoriteRecipes,
                            badges: user.badges
                    }});
                })
            } else {
                setAuthInit({loading: false, auth: {loggedIn: false}})
            }
        });
    }, []);
    return authInit;
}

async function read(id) {
    let response = await db
        .collection("users")
        .doc(id)
        .get();
    if (response === null || response === undefined) return null;
    return response.data();
}

async function read2(id) {
    db
        .collection("users")
        .doc(id)
        .onSnapshot((doc) => {
            if (doc !== null || doc !== undefined) {
                return doc.data();
            }
        });
}

Code for all the user his favorited recipes:

import {
    IonCard,
    IonCardContent,
    IonCardHeader,
    IonCardSubtitle,
    IonCardTitle,
    IonContent,
    IonHeader,
    IonPage,
    IonTitle,
} from '@ionic/react';
import { db } from '../firebase/firebase.utils';
import React, {useEffect, useState} from "react";
import {Recipe, toRecipe} from "../models/recipe";
import Header from "../components/Header";
import {useAuth} from "../auth";

const Favorites: React.FC = () => {
    const { userId, favoriteRecipes } = useAuth();
    const [recipes, setRecipes] = useState<Recipe[]>([]);

    useEffect(() => {
        const recipesRef = db.collection('recipes');
        favoriteRecipes.forEach( el => {
            let recipeRef = recipesRef.doc(el);
            recipeRef.onSnapshot ((doc) => {
                setRecipes(arr => [...arr, toRecipe(doc)]);
            });
        })
    }, [userId, favoriteRecipes]);

    return (
        <IonPage>
            <IonHeader>
                <Header />
            </IonHeader>
            <IonContent fullscreen>
                <IonTitle className="ion-padding">Favoriete Recepten</IonTitle>
                {recipes.map((entry, index) =>
                    <IonCard routerLink={`/my/recipes/view/${entry.id}`} key={index}>
                        <img src={entry.photo} alt={entry.title}/>
                        <IonCardHeader>
                            <IonCardSubtitle>{entry.userName}</IonCardSubtitle>
                            <IonCardTitle>{entry.title}</IonCardTitle>
                        </IonCardHeader>
                        <IonCardContent>{entry.description}</IonCardContent>
                    </IonCard>)}
            </IonContent>
        </IonPage>
    );
};

export default Favorites;

Well, this might not solve all your issues but I see that read2 does not seem to return a value, so that might explain a part of the error

Next, you THEN the result of the read so in case the app state is not updated - not sure how done in react - u need a re-read the data - maybe that explains the required reload?

Ideally in firebase u setup a realtime listener on the data which the app listens to. To avoid multiple listeners you use a state management lib/setup to have only one listener to firebase feeding the state

Huh? it does on return doc.data() right? or doesnt it work that way?

Edit: just learned that onsnapshot never returns a value so i have to use setState inside the onSnapshot on a state thats also use able by the useAuthInit function

There is no return statement in read2, so its a void function

But with your new approach i wonder if that is relevant

My new approach is not working though… I tried creating a global context for it but i cannot use that in a function…

Then i tried using a document variable (named dataTest) but that does not work either.

console.log(dt2, dataTest) gives
dt2 () => {
i.Xo(), t.asyncQueue.enqueueAndForget(async () => Bo(await Ya(t), r));
}

but i have no idea what that is or how to use that. Any ideas?

let dataTest = null;

// gebruikt door App.tsx om firebasegebruiker op te halen
export function useAuthInit(): AuthInit {
    const [authInit, setAuthInit] = useState<AuthInit>({loading: true});
    console.log(dataTest);
    useEffect(() => {
        return firebaseAuth.onAuthStateChanged((firebaseUser) => {
            if(firebaseUser){
                console.log(firebaseUser);
                read2(firebaseUser.uid).then((user ) => {
                    console.log('dt2',dataTest);
                    setAuthInit({
                        loading: false,
                        auth:  {
                            loggedIn: true,
                            userId: firebaseUser.uid,
                            userName: dataTest.username,
                            favoriteRecipes: dataTest.favoriteRecipes,
                            badges: dataTest.badges
                    }});
                })
            } else {
                setAuthInit({loading: false, auth: {loggedIn: false}})
            }
        });
    }, []);
    return authInit;
}

async function read(id) {
    let response = await db
        .collection("users")
        .doc(id)
        .get();
    if (response === null || response === undefined) return null;
    return response.data();
}

async function read2(id) {
    dataTest =  db.collection("users")
        .doc(id)
        .onSnapshot((doc) => {
            if (doc.data() !== null || doc.data() !== undefined) {
                 return dataTest =  doc.data();
            }
        });
}

Your read2 is still not returning anything … so the caller cannot expect to receive anything as part of the then((user)=> part… Which apparently you understood correctly in read (return response.data();)

Next the assignment to dataTest in read2 is messy as it first will be a promise that never resolves because you don’t THEN it. Later on you want to assign data.doc() to it.

async function read2(id) {
    return db.collection("users")
        .doc(id)
        .onSnapshot((doc) => {
            if (doc.data() !== null || doc.data() !== undefined) {
                  dataTest =  doc.data();  // should become obsolete 
                 return doc.data();
            } // there needs to be an else branch, otherwise the result will be void again, and the caller is not testing that.
        });
}

For the rest there is a bit too much React for my knowledge…

While i understand what you mean, i have no idea how to fix this…

I want the read2 function to return the values of doc.data(). Im still learning how to work with promises and async functions.

let dataTest = null;

// gebruikt door App.tsx om firebasegebruiker op te halen
export function useAuthInit(): AuthInit {
    const [authInit, setAuthInit] = useState<AuthInit>({loading: true});
    console.log(dataTest);
    useEffect(() => {
        return firebaseAuth.onAuthStateChanged((firebaseUser) => {
            if(firebaseUser){
                console.log('firebaseUser',firebaseUser);
                read2(firebaseUser.uid).then((data) => {
                    console.log('data from read2',data);
                    setAuthInit({
                        loading: false,
                        auth:  {
                            loggedIn: true,
                            userId: firebaseUser.uid,
                            userName: data.username,
                            favoriteRecipes: data.favoriteRecipes,
                            badges: data.badges
                    }});
                })
            } else {
                setAuthInit({loading: false, auth: {loggedIn: false}})
            }
        });
    }, []);
    return authInit;
}

async function read(id) {
    let response = await db
        .collection("users")
        .doc(id)
        .get();
    if (response === null || response === undefined) return null;
    return response.data();
}

async function read2(id) {
     console.log('getting id',id);
    return db.collection("users")
        .doc(id)
        .onSnapshot((doc) => {
           console.log('Getting doc',doc);
            if (doc.data() !== null || doc.data() !== undefined) {
console.log('Returning data');
                 return dataTest =  doc.data();
            } else {
             return {
 console.log('Returning empty stuff'); 
         userName:'', favoriteRecipes:'', badges:''
           }
}
        });
}

Curious to see the trace of things going on… Maybe you can try with this code?

I tried but it does not compile… still says data will always be void.

It’s pretty difficult to tell…

For starters, data from read contains content which is not data but a function.

I would make a minimal app and start building this part up from scratch while testing if it works… And use many console.logs to trace. Imho it seems your problem is about doing proper reads to firebase…

1 Like

Not entirily true, doc.data() contains the correct data in the read function. I just dont know how to propely pass it, i think the problem is with the way i handle promises and async functions…

Thanks for your time and help though! Appreciate it a lot

1 Like