Home > Net >  Firestore data not loading in UseEffect
Firestore data not loading in UseEffect

Time:10-14

I have a Firestore database set up in Firebase, where there is a user table consisting of a subcollection of sessions to which they belong. There is no data being stored in the session subcollection, only the ID is being used to correlate with that of a collection called sessions, which contains data such as the session's location.

Thus in my code, I am required to get the session ID(s) to which a specific user belongs, before I display the details about those session(s) on the page. I am having a strange error, however, in that if I change something on the page and save the document, my output will display, but if I refresh the page, no output is displayed and I receive an error saying that I am querying the session collection using an empty array of session IDs. I have no clue why the session IDs are not always being loaded.

The error I am getting: Uncaught (in promise) FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.

export default function Sessions() {
  const { currentUser } = useAuth();
  const [sessionIDs, setSessionIDs] = useState([]);
  const [sessions, setSessions] = useState([]);
  const sessionIDArray = [];



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

  function getSessionIDs() {
    console.log("first")
    const query = collection(users, currentUser.uid, "sessions");
    getDocs(query)
      .then((response) => {
        const sessionID = response.docs.map((doc) => ({
          id: doc.id
        }));
        setSessionIDs(sessionID);
      }).catch((error) => console.log(error.message));
  }

  const loadSessionIDsToArray = async () => {
    
    await getSessionIDs();
    console.log("second")
    sessionIDs.forEach(element => {
      sessionIDArray.push(element.id);
    })

    console.log(sessionIDArray)
  }

  const getSessionsUsingIDs = async () => {
    
    await loadSessionIDsToArray();    
    console.log("third")
    if (sessionIDArray.length!==0){
      const q = query(allSessions, where('__name__', "in", sessionIDArray));
      getDocs(q)
        .then((response) => {
          const session = response.docs.map((doc) => ({
            data: doc.data(),
            id: doc.id
          }));
          setSessions(session);
        }).catch((error) => console.log(error.message));
    }
  
    

  }



  return (
    <div>
      <h2>Awe {currentUser.uid}</h2>
      <ul>
        {sessions.map((session) => (
          <li key={session.id}>
            {session.data.location}
          </li>
        ))}
      </ul>
    </div>
  )
}

I believe there may be something wrong with the useEffect function itself.

Edit: I have found this forum, where the person is having the same problem as me, although I am still unable to get my code working: https://stackoverflow.com/questions/69036657/react-useeffect-only-works-if-i-make-a-change-to-the-code-once-the-page-is-open#:~:text=You have not added newListState,and the useEffect hook runs.

CodePudding user response:

Your problem boils down to improper Promise chaining. You have a mix of floating Promises, log messages and shared global variables that are leading to confusion because it makes it look like it's doing what it should but isn't actually doing it.

First, you should make the getSessionIDs independent of your component and lift it out of it.

function getSessionIDs(userId) { // <-- note switching to argument here
  const usersColRef = collection(getFirestore(), "users");
  const qUserSessions = collection(usersColRef, userId, "sessions"); // don't use 'query' as it conflicts with the query method of the Firebase lib
  return getDocs(qUserSessions) // <-- note return here
    .then((querySnapshot) => { // qs, qSnap or querySnapshot is a better choice here
      // return array of session IDs
      return querySnapshot.docs.map((doc) => ({
        id: doc.id
      }));
    }); // note removal of error suppression
}

or the shorter

async function getSessionIDs(userId) { // <-- note switching to argument here
  const usersColRef = collection(getFirestore(), "users");
  const qUserSessions = collection(usersColRef, userId, "sessions");
  const querySnapshot = await getDocs(qUserSessions);
  return querySnapshot.docs
    .map(({ id }) => ({ id })); // this shorthand only works for extracting IDs
}

Next, you can remove loadSessionIDsToArray as it is modifying a global variable, which will be eliminated from the corrected code.

Next, you should make the getSessionsUsingIDs function independent too (and rename it):

async function getSessionsData(userId) {    
  const sessionIDArray = await getSessionIDs(userId);

  if (sessionIDArray.length === 0) // this is known as the fail-fast/guard pattern
    return []; // no sessions atm

  // if here, we've got sessions to fetch

  const allSessionsColRef = collection(getFirestore(), "sessions");
  // WARNING: The 'in' operator can only handle up to 10 sessions at a time!
  const qSessionData = query(allSessionsColRef, where(documentId(), "in", sessionIDArray));
  const querySnapshot = await getDocs(qSessionData);
  return querySnapshot.docs
    .map((doc) => ({
      data: doc.data(),
      id: doc.id
    })); // note removal of error suppression
}

Note: To work around the 10 id maximum, take a look at the last code block of this answer and make use of its chunkArr function.

Then in your component, you would update the useEffect call to:

useEffect(() => {
  const userId = currentUser && currentUser.uid;

  if (!userId) {
    setSessions([]); // no user logged in, do nothing
  }

  // for tracking if component was detached
  let detached = false;
  
  getSessionsData(userId)
    .then((sessionsData) => {
      if (detached) return; // detached? do nothing
      setSessions(sessionsData);
    })
    .catch((err) => {
      if (detached) return; // detached? ignore error
      console.error('Failed to get user session data:', err);
    });
  
  return () => detached = true;
}, [currentUser && currentUser.uid]); // rerun when user changes

Bringing this together gives:

// Sessions.jsx
import { collection, documentId, getDocs, getFirestore, where, query } from "firebase/firestore";

async function getSessionIDs(userId) {
  /* as above */
}

async function getSessionsData(userId) {
  /* as above */
}

export default function Sessions() {
  const { currentUser } = useAuth();
  const [sessions, setSessions] = useState([]);

  const userId = currentUser && currentUser.uid;

  useEffect(() => {
    /* as above */
  }, [userId]);

  if (!userId)
    return (<div>Not logged in!</div>)

  return (
    <div>
      <h2>Awe {userId}</h2>
      <ul>
        {sessions.map((session) => (
          <li key={session.id}>
            {session.data.location}
          </li>
        ))}
      </ul>
    </div>
  )
}
  • Related