Home > Software engineering >  Const is not defined -> Yet standard solutions don't work
Const is not defined -> Yet standard solutions don't work

Time:02-17

I want to display a mapped list where "UserName" is an entry value from a Firebase Realtime Database corresponding to the author of each entry.

The following code, inside the get(UsernameRef).then((snapshot) =>{}) scope, returns an undefined reference error as expected, 'UserName' is assigned a value but never used and 'UserName' is not defined

  const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
  React.useEffect(() => {
    const RecipeLibraryRef = ref(db, "Recipes/");
    onValue(RecipeLibraryRef, (snapshot) => {
      const RecipeLibrary = [];
      snapshot.forEach((child) => {
        const AuthorUserId = child.key;

        child.forEach((grandChild) => {
          const UserNameRef = ref(db, "Account/"   AuthorUserId   "/username");
          get(UserNameRef).then((snapshot) => {
            const UserName = snapshot.val();
          });
          RecipeLibrary.push({
            name: grandChild.key,
            author: UserName,
            ...grandChild.val(),
          });
        });
      });
      setRecipeLibrary(RecipeLibrary);
      console.log({ RecipeLibrary });
    });
  }, []);

I've tried:

  • Using a React state to pass the variable -> Can't use inside React useEffect
  • Exporting and Importing a separate function that returns the desired UserName -> return can only be used in the inner scope
  • Moving the list .push inside the Firebase get scope -> React.useState can no longer access the list

I'm hoping there is a simple solution here, as I am new. Your time and suggestions would mean a lot, thank you!

Update:

I got the RecipeLibrary array to contain the desired "UserName" entry, named author by moving the array .push inside the .then scope. Here is a log of that array at set (line 59) and at re-render (line 104).

    child.forEach((grandChild) => {
      const UserNameRef = ref(db, "Account/"   AuthorUserId   "/username");
      get(UserNameRef).then((snapshot) => {
        const UserName = snapshot.val();
        RecipeLibrary.push({
          name: grandChild.key,
          author: UserName,
          authorId: AuthorUserId,
          ...grandChild.val(),
        });
      });
    });
  });
  setRecipeLibrary(RecipeLibrary);
  console.log(RecipeLibrary);

However, now the mapped list is not rendering at all on screen.

Just some added context with minimal changes to original code, been stuck on this so long that I'm considering a full re-write at this point to jog my memory. Oh and here is the bit that renders the mapped list in case:

<Box width="75%" maxHeight="82vh" overflow="auto">
            {RecipeLibrary.map((RecipeLibrary) => (
              <Paper
                key={RecipeLibrary.name}
                elevation={3}
                sx={{

etc...

CodePudding user response:

This is a tricky one - the plainest option might be to move push() and setRecipeLibrary() inside the then() callback so they're all within the same scope, but that would have some terrible side effects (for example, triggering a re-render for every recipe retrieved).

The goal (which you've done your best to achieve) should be to wait for all the recipes to be loaded first, and then use setRecipeLibrary() to set the full list to the state. Assuming that get() returns a Promise, one way to do this is with await in an async function:

const [RecipeLibrary, setRecipeLibrary] = React.useState([]);
React.useEffect(() => {
  const RecipeLibraryRef = ref(db, "Recipes/");
  onValue(RecipeLibraryRef, (snapshot) => {

    // An async function can't directly be passed to useEffect(), and
    // probably can't be accepted by onValue() without modification,
    // so we have to define/call it internally.
    const loadRecipes = async () => {
      const RecipeLibrary = [];

      // We can't use an async function directly in forEach, so
      // we instead map() the results into a series of Promises
      // and await them all.
      await Promise.all(snapshot.docs.map(async (child) => {
        const AuthorUserId = child.key;

        // Moved out of the grandChild loop, because it never changes for a child
        const UserNameRef = ref(db, "Account/"   AuthorUserId   "/username");

        // Here's another key part, we await the Promise instead of using .then()
        const userNameSnapshot = await get(UserNameRef);
        const UserName = userNameSnapshot.val();

        child.forEach((grandChild) => {    
          RecipeLibrary.push({
            name: grandChild.key,
            author: UserName,
            ...grandChild.val(),
          });
        });
      }));
      
      setRecipeLibrary(RecipeLibrary);
      console.log({ RecipeLibrary });
    };

    loadRecipes();
  });
}, []);

Keep in mind that Promise.all() isn't strictly necessary here. If its usage makes this less readable to you, you could instead execute the grandChild processing in a plain for loop (not a forEach), allowing you to use await without mapping the results since it wouldn't be in a callback function.

If snapshot.docs isn't available but you can still use snapshot.forEach(), then you can convert the Firebase object to an Array similar to Convert A Firebase Database Snapshot/Collection To An Array In Javascript:

// [...]

      // Change this line to convert snapshot
      // await Promise.all(snapshot.docs.map(async (child) => {
      await Promise.all(snapshotToSnapshotArray(snapshot).map(async (child) => {

// [...]

// Define this somewhere visible
function snapshotToSnapshotArray(snapshot) {
    var returnArr = [];

    snapshot.forEach(function(childSnapshot) {
        returnArr.push(childSnapshot);
    });

    return returnArr;
}

Note that if get() somehow doesn't return a Promise...I fear the solution will be something less straightforward.

  • Related