This question already has answers here:
How to async await in react render function?
(2 answers)
Which react hook to use with firestore onsnapshot?
(4 answers)
React native call function in text element
(1 answer)
FireStore - Update the UI with new data without reloading the browser
(2 answers)
Closed 1 year ago.
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>
);
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.
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>
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.
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
Related
Many articles writing about how to return pending promise and work with React suspense but it's not working in real world.
They don't consider if the component got visited second time, and it won't refetch the data from the server.
e.g. => https://dev.to/darkmavis1980/a-practical-example-of-suspense-in-react-18-3lln?signin=true
The below example would only work for the first time we visit the component but not re-fetch data for the following times.
Any idea to let it work to prevent not doing re-fetching?
Component
const dataFetchWithWrapPromise = (url) => {
return wrapPromise(window.fetch(url, {
}));
}
const resource = dataFetchWithWrapPromise('http://localhost:3000/data');
function Articles() {
const data = resource.read();
React.useEffect(() => {
return () => {
resource.reset();
}
}, []);
return (
<>
<h1>Data</h1>
<pre>
{JSON.stringify(data, null, 4)}
</pre>
</>
);
}
export default Articles;
function wrapPromise(promise) {
let status = 'pending';
let response;
const suspender = promise.then(
async res => {
status = 'success';
response = await res.json();
},
err => {
status = 'error';
response = err;
},
);
const handler = {
pending: () => {
throw suspender;
},
error: () => {
throw response;
},
success: () => {
console.log(response)
return response
},
default: () => {
throw suspender;
},
};
const read = () => {
const result = handler[status] ? handler[status]() : handler.default();
return result;
};
const reset = () => {
if(status!=='pending') {
status = 'pending';
response = undefined;
}
}
return { read, reset };
}
export default wrapPromise;
Ok, so I think I got you covered. It so happens that I liked <Suspense> ever since I heard of it. I stumbled with it in my learning of asynchronous JavaScript because I was coding wj-config. This preface is just to let you know that I'm no React master, but it so happens that I ended up creating a React example for wj-config v2.0.0, which is currently in BETA 2. This example does what you want.
So no more chit-chat. The code of interest is here.
It is a table component that loads person data from Mockaroo. The web page (parent) has two controls to specify the number of rows wanted as well as the minimum birth date wanted. Whenever the value of any of those controls change, the person data is re-fetched. The table itself uses <Suspense> in two places.
The component module starts by defining the fetching functions needed for person and country data. Then it declares some variables that are captured in scopes later on. The starting promise is required for the first render. Its resolver is exposed through startingResolver, and the starting promise is wrapped as per the <Suspense> mechanics that you clearly know.
Focus your attention now to the PersonsTable function. It sets up a useEffect call to re-trigger the data fetching operations based on changes of props. As I'm not a super master in ReactJS, maybe there's a better way than props. I just know props will trigger the effect automatically, so I used them.
On the first render, the starting promise is thrown, but it will never be fulfilled since it is a bogus promise. The code inside useEffect makes this promise resolve at the same time the fetching promise resolves. Then, using the fetching promise, the readPersons function is defined.
NOTE: I'm not a native English speaker. Pardon my horrible "persons" instead of "people" mistake. :-( I'll correct whenever I have time.
Anyway, with this set up, you'll have completed your goal. The linked code sample goes beyond this by having an inner <Suspense> that waits for country data, but I suppose an explanation is not needed since I believe the question is now covered.
Hope this helps!
Im having some trouble with realtime database and react native.
I have the following useEffect in a component that is supposed to listen for changes and then update state as required, I then use that state to populate a list.
The component gets a data object passed as a prop, the data object contains a string array called members that contains uuids, I am trying to iterate over those to get the attached user from realtime db and then save those objects to a state array.
const myComponent = ({ data }) => {
const [users, setUsers] = useState([]);
useEffect(() => {
const userArr = [];
data.map(item => {
item.members.forEach((username: string) => {
database()
.ref(`users/${username}`)
.on('value', snapshot => {
userArr.push(snapshot.val());
});
});
});
setUsers(userArr);
};
}, []);
return (
<>
{users} <----- this is in a flatlist
</>
);
}
It works eventually after refreshing the screen about 5 times. Any help would be greatly appreciated.
The simplest way to get some data to show is to move update the state right after you add an item to the array:
useEffect(() => {
const userArr = [];
data.map(item => {
item.members.forEach((username: string) => {
database()
.ref(`users/${username}`)
.on('value', snapshot => {
userArr.push(snapshot.val());
setUsers(userArr); // 👈
});
});
});
};
}, []);
Now the UI will update each time that a user is loaded from the database.
I also recommend reading some more about asynchronous loading, such as in Why Does Firebase Lose Reference outside the once() Function?
Your database call may be asynchronous, which is causing the code inside the useEffect to act a little funny. You could push all those database calls (while iterating through item.members) into an array, and then do Promise.all over the array. Once the promises are resolved, you can then set the users.
Hope this helps!
add an async function inside useEffect and call it
useEffect(() => {
const getUsers = async () => {
const userArr = [];
data.....
//wait for it with a promise
Promise.all(userArr).then(array => setUsers(array))
})
getUsers()
}, [])
not sure if the function needs to be async
I have the following action, making an asynchronous GET request:
export const getPolls = () => {
return async dispatch => {
try {
const polls = await serv.call('get', 'poll');
dispatch(setPolls(polls));
dispatch(removeError());
} catch (err) {
const error = err.response.data;
dispatch(addError(error.message));
}
}
}
Then, in component Polls, I want to be able to call this action, so I can show the list of Polls. To do this, I pass it on to this component's props:
export default connect(store => ({
polls: store.polls
}), {
getPolls
})
(Polls);
And access it through const {getPolls} = props;
I am using React Hooks to create and change the Polls component state. Like this:
const Polls = (props) => {
const {getPolls} = props
const [polls, setPolls] = useState([])
useEffect(() => {
const result = getPolls()
console.log(result)
setPolls(result)
}, [])
const pollsList = polls.map(poll => (<li key={poll._id}>{poll.question}</li>))
return (
<div>
<ul className='poll-list'>{pollsList}</ul>
</div>
)
}
With this code, I'm not able to get the polls. When I console.log the result from calling getPolls(), I can see I'm obtaining a Promise. However, since getPolls() is an async function, shouldn't this be avoided? I believe the problem has something to do with the way I'm using React Hooks, particularly useEffect, but I can't figure it out.
Thank you.
When I console.log the result from calling getPolls(), I can see I'm obtaining a Promise. However, since getPolls() is an async function, shouldn't this be avoided?
You have a fundamental misunderstanding of async functions. async and await are just syntaxes that help you deal with Promises. An async function always returns a Promise. In order to get an actual value, you would have to await the value from inside another async function.
But your getPolls() function is not a function that returns the polls. It doesn't return anything. It fetches the polls and calls dispatch with the data to store the polls in your redux store.
All that you need to do in your component is call getPolls so that this code is executed. Your connect HOC is subscribing to the current value of polls from store.polls and the polls prop will update automatically when the getPolls function updates store.polls (which it does by calling dispatch(setPolls(polls))).
Your component can be simplified to this:
const Polls = (props) => {
const {polls, getPolls} = props;
// effect calls the function one time when mounted
useEffect(() => {
getPolls();
}, [])
const pollsList = polls.map(poll => (<li key={poll._id}>{poll.question}</li>))
return (
<div>
<ul className='poll-list'>{pollsList}</ul>
</div>
)
}
We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}
I'm having a problem making multiple request in a loop.
I'm making a react app that renders multiple components called Cards. inside each card I want to make some requests so I got this.
componentWillMount(){
if(this.props.movies){
let promises = []
this.props.movies.forEach((item, i) => {
console.log(item)
let movieUrl = `http://localhost:3000/movies/${item}`
promises.push(axios.get(movieUrl))
})
axios.all(promises).then(res => console.log(res))
}
}
Movies is an array that I get from the father component.
so apparently is working because I get results but tit is always with the last element of the last card. Here is an image:
You should avoid using forEach when you really need to map and build the url with item.imdbID instead of item
componentWillMount(){
if(this.props.movies){
const promises = this.props.movies.map(item => {
const movieUrl = `http://localhost:3000/movies/${item.imdbID}`
console.log(movieUrl)
return axios.get(movieUrl)
)
Promise.all(promises).then(results => console.log(results))
}
}
Edit1: removed async/await due to incompatible build configuraton
Edit2: used item.imdbID instead of item and logged urls
You can use async/await. Look:
async componentWillMount(){
if(this.props.movies){
const results = []
this.props.movies.forEach((item, i) => {
const movieUrl = `http://localhost:3000/movies/${item}`
const result = await axios.get(movieUrl)
results.push(result)
})
// console.log(results)
}
}
Have you tried using bluebird's Promise.mapSeries?
import Promise from 'bluebird'
componentWillMount(){
if(this.props.movies){
Promise.resolve(this.props.movies)
.mapSeries(item => axios.get(`http://localhost:3000/movies/${item}`))
.then(movies => console.log(movies))
}
}