0

I have a Firebase DB with a React-Native project using Expo. I can get data from DB with no problem. The problem becomes when I try to do a nested Firebase query for a many to many relationship. Here's my piece of code:


    useEffect(() => {
        setUser(firebase.auth().currentUser);
    });

    useEffect(() => {
        if (user) {
            async function getCertificates() {
                await firebase
                    .database()
                    .ref("/user_fav")
                    .child(user.uid)
                    .on("value", (querySnapShot) => {
                        const cert = [];
                        const temp = [];
                        querySnapShot.forEach((item) => {
                            temp.push(item.key);
                            firebase
                                .database()
                                .ref("/certificates")
                                .child(item.key)
                                .on("value", (querySnapShot) => {
                                    cert.push({
                                        name: querySnapShot.val().name,
                                        description: querySnapShot.val()
                                            .description,
                                        years: querySnapShot.val().years,
                                        key: querySnapShot.val().key,
                                    });
                                });
                        });
                        setData(cert);
                        setLoading(false);
                    });
            }

            getCertificates();
        }
    }, []);

First of all, I get the user for UID. I make a query asking for the certificates of a particular user (success, temp array is filled). Then, I try to get the certificates but here's the problem.

The first time Expo renders the App, I can't see the certificates. The array is empty. When I save the document (and Expo re-render) then the certificates appears.

It's very weird and I don't know where the problem is.

Thank you very much.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
miki_kanez
  • 43
  • 4

1 Answers1

1

All data is loaded from Firebase asynchronously.

Your setDate(cert) now runs before your cert.push(...) has run.

You can most easily see this by placing some logging statements:

console.log("Before attaching first listener");
firebase
    .database()
    .ref("/user_fav")
    .child(user.uid)
    .on("value", (querySnapShot) => {
        console.log("Got first data");
        querySnapShot.forEach((item) => {
            console.log("Before attaching nested listener");
            firebase
                .database()
                .ref("/certificates")
                .child(item.key)
                .on("value", (querySnapShot) => {
                    console.log("Got second data");
                });
        });
        console.log("After attaching nested listeners");
    });
console.log("After attaching first listener");

When you run this code, it prints:

Before attaching first listener

After attaching first listener

Got first data

Before attaching nested listener (probably multiple of this)

After attaching nested listeners

Got second data (as many as the nested listeners you added)

So by the time you're calling setState, the cert hasn't been populated with data from the database yet.

The solution is always the same: any code that needs data from the database, needs to be inside the callback that gets that data, or be called from there.

So in your case:

await firebase
    .database()
    .ref("/user_fav")
    .child(user.uid)
    .on("value", (querySnapShot) => {
        const cert = [];
        const temp = [];
        querySnapShot.forEach((item) => {
            temp.push(item.key);
            firebase
                .database()
                .ref("/certificates")
                .child(item.key)
                .on("value", (snapshot) => {
                    cert.push({
                        name: snapshot.val().name,
                        description: snapshot.val()
                            .description,
                        years: snapshot.val().years,
                        key: snapshot.val().key,
                    });
                    setData(cert);
                    setLoading(false);
                });
        });
    });

The above sets the data each time one of your nested listeners gets any data. If you only want to do this once all of them have received data at least once, you can keep a counter to track the number of callbacks that have received data and compare that to querySnapShot.length.

Also see:

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • You're amazing! Thank you very much for your response! Now I understand how it works! Now, I mesure the length of querySnapShot (with .numChildren();) and then I call setData(cert) and setLoading(false). And it works! Thank you again ^^ – miki_kanez Jul 08 '20 at 07:42