Home > other >  How to fetch data from all documents in a subcollection in firebase firestore using React?
How to fetch data from all documents in a subcollection in firebase firestore using React?

Time:01-29

For my project, I want to fetch data from all documents in a subcollection. And there are multiple documents with this subcollection.

To clarify, this is how my firestore is strctured: I have an events collection which contains multiple documents with doc.id being the event name itself. Each event document has several fields and an attendee subcollection. In the attendee subcollection, each document contains details about the attendee.

I want to map through all documents in the events collection and fetch data about attendees from all of them.

And I want to display this data when the component first renders. So I'm calling the function inside useEffect. Here's what I have tried:

const [attendeeInfo, setAttendeeInfo] = useState({});
    const [events, setEvents] = useState([]);

    const getEventsData = async () => {
        // first of all, we need the name of the user's org
        // fetch it from users collection by using the uid from auth
        const orgRef = doc(db, "users", auth["user"].uid);
        const orgSnap = await getDoc(orgRef);

        // now that we have the org name, use that to search events...
        // ...created by that org in events collection
        const eventsRef = collection(db, "events");
        const eventsQuery = query(eventsRef, where("createdBy", "==", orgSnap.data().orgName));
        const eventsQuerySnapshot = await getDocs(eventsQuery);
        let eventsInfo = [];

        eventsQuerySnapshot.forEach((doc) => {

            eventsInfo.push(doc.id);

        })

        setOrg(orgSnap.data().orgName);
        setEvents(eventsInfo);
    }

    const getAttendeesData = (events) => {
        console.log(events);
        let attendeeInformation = [];

        events.forEach(async (event) => {
            const attendeesRef = collection(db, "events", event, "attendees");
            const attendeesSnap = await getDocs(attendeesRef);

            attendeesSnap.forEach((doc) => {

                const isItMentor = doc.data().isMentor ? "Yes" : "No";
                const isItMentee = doc.data().isMentee ? "Yes" : "No";
                const attendeeData = {
                    name: doc.id,
                    mentor: isItMentor,
                    mentee: isItMentee,
                };

                attendeeInformation.push(attendeeData);
            })

        })

        // console.log(attendeeInformation);
        setAttendeeInfo(attendeeInformation);

    }


    useEffect(() => {

        getEventsData();
        // console.log(attendeeInfo);
        getAttendeesData(events);
    }, []);

However, when I console log the events inside my attendeesData function, I get an empty array which means that the events state variable hasn't been updated from previous function.

Can anyone help me solve this?

CodePudding user response:

This is a timing issue. On first render you start fetching the list of events, but you aren't waiting for them to be retrieved before using them. Furthermore, because you only run this code on mount, when events is eventually updated, getAttendeesData won't be invoked with the updated array.

useEffect(() => {
    getEventsData(); // <-- queues and starts "fetch event IDs" action
    getAttendeesData(events); // <-- starts fetching attendees for `events`, which will still be an empty array
}, []); // <-- [] means run this code once, only when mounted

The solution to this is to split up the useEffect so each part is handled properly.

useEffect(() => {
    getEventsData(); // <-- queues and starts "fetch event IDs" action
}, []); // <-- [] means run this code once, only when mounted

useEffect(() => {
    getAttendeesData(events); // initially fetches attendees for an empty array, but is then called again when `events` is updated with data
}, [events]); // <-- [events] means run this code, when mounted or when `events` changes

Next, you need to fix up getAttendeesData as it has a similar issue where it will end up calling setAttendeeInfo() at the end of it with another empty array (attendeeInformation) because you aren't waiting for it to be filled with data first. While this array will eventually fill with data correctly, when it does, it won't trigger a rerender to actually show that data.

const [attendeeInfo, setAttendeeInfo] = useState([]); // <-- should be an array not an object?
const [events, setEvents] = useState([]);

const getAttendeesData = async (events) => {
    console.log(events);

    const fetchAttendeesPromises = events.map(async (event) => {
        const attendeesRef = collection(db, "events", event, "attendees");
        const attendeesSnap = await getDocs(attendeesRef);
        const attendeeInformation = [];

        attendeesSnap.forEach((doc) => {
            const isItMentor = doc.data().isMentor ? "Yes" : "No";
            const isItMentee = doc.data().isMentee ? "Yes" : "No";
            const attendeeData = {
                name: doc.id,
                mentor: isItMentor,
                mentee: isItMentee,
            };

            attendeeInformation.push(attendeeData);
        })

        return attendeeInformation; // also consider { event, attendeeInformation }
    })

    // wait for all attendees to be fetched first!
    const attendeesForAllEvents = await Promises.all(fetchAttendeesPromises)
      .then(attendeeGroups => attendeeGroups.flat()); // and flatten to one array

    // console.log(attendeesForAllEvents);
    setAttendeeInfo(attendeesForAllEvents);
}

Applying these changes in a basic and incomplete (see below) way, gives:

// place these outside your component, they don't need to be recreated on each render
const getEventsData = async () => { /* ... */ }
const getAttendeesData = async (events) => { /* ... */ }

export const YourComponent = (props) => {
  const [attendeeInfo, setAttendeeInfo] = useState(null); // use null to signal "not yet loaded"
  const [events, setEvents] = useState(null); // use null to signal "not yet loaded"
  const loading = events === null || attendeeInfo === null;

  useEffect(() => {
    getEventsData();
  }, []);

  useEffect(() => {
    if (events !== null) // only call when data available
      getAttendeesData(events);
  }, [events]);

  // hide component until ready
  // consider rendering a spinner/throbber here while loading
  if (loading)
    return null;

  return (
    /* render content here */
  )
}

Because getEventsData() and getAttendeesData() are Promises, you should make use of useAsyncEffect implmentations like @react-hook/async and use-async-effect so you can handle any intermediate states like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.

  •  Tags:  
  • Related