0

I have this code that gets all players in a specific lobby. The console.log() before the return works as expected:

  const getPlayersByLobby = async (lobbyId) => {
    const playersRef = firebase.firestore().collection("GameLobbies").doc(lobbyId).collection("players");
    const playerNames = [];
    const players = await playersRef.get();
    players.forEach((groupSnapshot) => {
       playerNames.push(groupSnapshot.data().name)
    });
    console.log(playerNames) // ["player1Name", "player2Name"];
    return playerNames
  }

Now when rendering the component I do a console.log(getPlayersByLobby(lobby.id)) to see what I get. I see I have a pending promise instead of my array.

Can anyone help my why this is? And how to get my array back?

  return(
    <div className="App">
      <h1>Current game lobbies:</h1>
      {gameLobbies.map((lobby) => { //gameLobbies comes from a useState() hook that does work
        return (<div key={lobby.id}>
          <h2>naam: {lobby.name}</h2>
          <p>Code: {lobby.gameId}</p>
          <h3>Spelers in deze lobby:</h3>
          <ul>
            {console.log(getPlayersByLobby(lobby.id))}
          </ul>
        </div>
        )
      })}
    </div>
  );
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Perynal
  • 21
  • 2
  • try checking the typeof returned value and leave a comment about that – codingwith3dv Jun 03 '21 at 12:05
  • @codingwith3dv obviously, the result of an "unawaited" `async` function is a promise. – derpirscher Jun 03 '21 at 12:07
  • @codingwith3dv console.log(typeof(getPlayersByLobby(lobby.name))) actually showed this to be an object – Perynal Jun 03 '21 at 12:16
  • Since `playerNames` is loaded asynchronously, you need to store its value in the state of the component. The simplest way to do that is through `useState` as Josh shows here: https://stackoverflow.com/a/59959589 and I've shown here: https://stackoverflow.com/a/67511632, and here: https://stackoverflow.com/a/62565426 – Frank van Puffelen Jun 03 '21 at 14:01

4 Answers4

2

This is something I struggled with when I came across promises.

With promises it is important to remember that async/await is just a syntactic sugar and it is doesn't make the code synchronous as one might think. It lets you write your code in a way in which it looks synchronous.

Any async function by default returns a Promise and the only way to get the returned value (which comes AFTER the promise has resolved) is to await it everywhere you call that async function.

It does not matter that you already awaited it within the function, that doesn't mean that the function will wait for the promise to get the value/resolve the value before it returns.

The function simply returns the Promise right away with pending status and once the data is there the Promise is updated with the data. In this case by not adding await you are saying "I don't want to wait for the data, just give me the promise" and now you can pass this promise around but you will have to await it if you want the value anywhere you pass it.

Since in your example in the component {console.log(getPlayersByLobby(lobby.id))} there is no await before you call getPlayersByLobby it will simply return a pending Promise without awaiting it

Another way to think about it is using .then(()=> {}).catch((error) => {}) syntax. If there were no async/await keywords or Promises you would have to chain all thens inside of each other.

Using async/await you STILL have to chain the promise but it just looks more readable/synchronous

Hope that makes sense.

Vin_it
  • 186
  • 1
  • 5
1

try to store players in the state when you get them from the database, also its better to use useEffect when you retrieve data from your database

const [players, setPlayers] = useState([])
const [lobbyId, setLobbyId] = useState([initalId])

 useEffect(async () => {
    const playersRef = 
  firebase.firestore().collection("GameLobbies").doc(lobbyId).collection("players");
    const playerNames = [];
    const players = await playersRef.get();
    players.forEach((groupSnapshot) => {
       playerNames.push(groupSnapshot.data().name)
    });
    console.log(playerNames) // ["player1Name", "player2Name"];
    //return playerNames
    setPlayers(playerNames)
  }, [lobbyId]) // it called again whenever you changed your looby id

now it should work

<ul>
   // now you have access to them
   {console.log(players)}
 </ul>
Mohammad Esam
  • 502
  • 3
  • 10
  • How would you give lobbyId as a parameter in a useEffect hook? Also does this work if there are multiple lobbies that all have unique players. Don't you dump all players in 1 players array? – Perynal Jun 03 '21 at 12:36
  • oh! sorry i missed this, check the answer – Mohammad Esam Jun 03 '21 at 12:46
  • ya it works with multiple lobbies once you change the `lobbyId` the ` useEffect` will run again with the updated lobby id and pull different players – Mohammad Esam Jun 03 '21 at 12:49
1

First, as you are only using the name field stored in your document, consider switching out groupSnapshot.data().name for groupSnapshot.get("name") for efficiency.

As this function is stateless, you should place this function outside of your component's render function:

const getPlayersByLobby = async (lobbyId) => {
    const playersRef = firebase.firestore()
        .collection("GameLobbies")
        .doc(lobbyId)
        .collection("players");
    const playerNames = [];
    const players = await playersRef.get();
    players.forEach((groupSnapshot) => {
        playerNames.push(groupSnapshot.get("name"))
    });
    console.log(playerNames) // ["player1Name", "player2Name"];
    return playerNames
}

Same with this "render lobby" function:

const renderLobby = (lobby, playerList = undefined) {
  let players;

  if (playerList === undefined) {
    players = (<p key="loading">Loading player list...</p>);
  } else if (playerList === null) {
    players = (<p key="failed">Failed to get player list</p>);
  } else if (playerList.length === 0) {
    players = (<p key="empty">Player list empty</p>);
  } else {
    players = (<ul key="players">{
      playerList.map((p) => (
        <li key={p.id}>
          {p.name}
        </li>
      ))
    }</ul>);
  }
 
  return (
    <div key={lobby.id}>
      <h2>naam: {lobby.name}</h2>
      <p>Code: {lobby.gameId}</p>
      <h3>Spelers in deze lobby:</h3>
      { players }
    </div>
  );
}

Next, in your component, you make use of useEffect() to fetch data from the database. Here, I've used a state object that is a map of lobby IDs to lists of players. On each render, a call is sent off to the database to fetch the current player list.

Because Promises (in this case, database calls) can take longer than render cycles to finish, we need to make sure to not call any setState functions after the component has been unmounted. This is done using the disposed boolean value to discard the results of the Promises if the useEffect's unsubscribe function has been called.

const [gameLobbies, setGameLobbies] = useState(/* init value */);
const [playersByLobby, setPlayersByLobby] = useState({});
useEffect(() => {
    let disposed = false;
 
    const newPlayersByLobby = {};

    Promise.all(gameLobbies.map((lobby) =>
      getPlayersByLobby(lobby.id)
        .then((playerList) => newPlayersByLobby[lobbyId] = playerList)
        .catch((err) => {
          console.error(`Failed to get Lobby #${lobbyId}'s player list: `, err);
          newPlayersByLobby[lobbyId] = null;
        })
    ))
      .then(() => {
        if (disposed) return; // component disposed/gameLobbies was changed
        setPlayersByLobby(newPlayersByLobby);
      })
    
    return () => disposed = true;
}, [gameLobbies]); // rerun when gameLobbies updates

return (
  <div className="App">
    <h1>Current game lobbies:</h1>
    {
      gameLobbies.map(
        (lobby) => renderLobby(lobby, playersByLobby[lobby.id])
      )
    }
  </div>
);

Note: Consider implementing a <Lobby> component along with a <PlayerList> component. By doing this, you allow yourself the ability to make full use of the Firestore's Realtime updates.

samthecodingman
  • 23,122
  • 4
  • 30
  • 54
0

Try doing it like this

{
  getPlayeesByLobby(lobby.id).then(val => {
    console.log(val);
  }
}

This should give an error in React so what's next

Make an iife or more specifically useEffect in React so make a useState and after getting data from backend setPlayers to that array. And then map the array and render <li> in the <ul>.

This is what you need to do

codingwith3dv
  • 359
  • 3
  • 8
  • pretty close, I get the array I want when I do that. But react gives me this error now: Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead. Pretty weird since it is an array... – Perynal Jun 03 '21 at 12:47