hi i have to refresh my page to see the effect of the person adding an event to the calendar:
my code is
const handleDateClick = async (DateClickArg) => {
if (DateClickArg) {
const title = prompt("Enter title", DateClickArg.dateStr); // allows user to put a title in
// making object
const event = {
title: title ? title : DateClickArg.dateStr,
start: DateClickArg.date,
allDay: true,
};
allEvents.push(event);
const db = fire.firestore();
let currentUserUID = fire.auth().currentUser.uid;
const doc = await fire
.firestore()
.collection("userCalendar")
.doc(currentUserUID)
.get();
db.collection("userCal/" + currentUserUID + "/activities").add({ event });
}
};
and my getuserinfo is:
const getUserInfo = async () => {
let currentUserUID = fire.auth().currentUser.uid;
const qSnap = await fire
.firestore()
.collection("userCal")
.doc(currentUserUID)
.collection("activities")
.get();
const data = [];
data = qSnap.docs.map((d) => ({
id: d.id,
title: d.data().event.title,
start: d.data().event.start.toDate(),
allDay: d.data().event.allDay,
...d.data(),
}));
//setData(data)
console.log(data);
setData([...data]);
};
useEffect(() => {
let mounted = false;
if (!mounted) {
getUserInfo();
}
return () => {
mounted = true;
};
}, []);
where am i going wrong with my use effect? is there a way for the data to update in the browser once its added to firebase? i am using react full calendar
Using get() only returns a point-in-time snapshot of your data. If you want to listen for realtime updates, use .onSnapshot() instead.
You'll also need to make sure you unsubscribe from updates when your component is cleaned up
useEffect(() => {
const currentUserUID = fire.auth().currentUser.uid;
return fire
.firestore()
.collection("userCal")
.doc(currentUserUID)
.collection("activities")
.onSnapshot(({ docs }) => {
setData(
docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
title: data.event.title,
start: data.event.start.toDate(),
allDay: data.event.allDay,
...data,
};
})
);
});
}, []);
.onShapshot() returns an unsubscribe function so returning that from your effect hook will run it when your component is unmounted.
Assuming your firebase call is ok, there is an error inside your useEffect call. You are setting the mounted variable wrong, it is supposed to be false when your component is destroyed and true after your component is rendered. Also, to avoid unexpected behaviors I highly recommend using the useRef hook to check that.
function Component() {
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true;
if (isMounted) {
getUserInfo();
}
return () => { isMounted.current = false }
}, []);
...
}
export default Component;
Related
I have listend an event in customhook and when that event works, I have to do some logic there with state.but now I only get empty array every time that event callback works.
const useChatHistoryList = () => {
const sk = useSocket();
const [chatList, setChatList] = useState([]);
const [end, setEnd] = useState(true);
useEffect(() => {
sk.emit('chatlist');
}, [start]);
useEffect(() => {
const onChatListReceived = data => {
const _data = JSON.parse(data);
setHistoryLoading(false);
setChatList(_data);
};
const onChatListToUpdateReceived = data => {
const _data = JSON.parse(data);
console.log(chatList);//getting only empty array everytime
};
sk.on('chatlist', onChatListReceived);
sk.on('chatlistToUpdate', onChatListToUpdateReceived);
return () => {
sk.off('chatlistToUpdate');
sk.off('chatlist');
};
}, []);
return { chatList,end};
};
Try to log your data first to make sure the data is there, then set your state with the data.
const [state, setState]= useState([]);
const _onReceived = (data) => {
// Here is your data from socket
console.log(data);
// Then set state value with data
setState(data);
}
useEffect(()=>{
// Init socket listener
socket.on("event", _onReceived);
}, []);
// This effect will runs everytime state value is set (including when setting default value)
useEffect(()=>{
// Actual 'state' value
console.log('State value: ', state);
}, [state]);
==========================
Edit, related to your updated codes in the question
Your onChatListToUpdateReceived function brings empty default value to the listener even later when it’s updated, your listener will still recognize chatList value as an empty string. Try to move out onChatListToUpdateReceived outside useEffect.
const onChatListToUpdateReceived = data => {
const _data = JSON.parse(data);
console.log(chatList);//getting only empty array everytime
};
useEffect(() => {
const onChatListReceived = data => {
const _data = JSON.parse(data);
setHistoryLoading(false);
setChatList(_data);
};
sk.on('chatlistToUpdate', onChatListToUpdateReceived);
return () => {
sk.off('chatlistToUpdate');
sk.off('chatlist');
};
}, []);
useEffect(() => {
sk.off('chatlistToUpdate');
sk.on('chatlist', onChatListReceived);
}, [chatList]);
I have not used socket.io before but this is what I meant by asynchronous update. From your code, it looked to me like your callback is getting called before the state is updated. So to solve this, I added a useEffect() with chatList as a dependency so that callback gets called every time chatList gets updated. I hope this makes sense.
const useChatHistoryList = () => {
const sk = useSocket();
const [chatList, setChatList] = useState([]);
const [end, setEnd] = useState(true);
const onChatListReceived = data => {
const _data = JSON.parse(data);
setHistoryLoading(false);
setChatList(_data);
};
const onChatListToUpdateReceived = data => {
const _data = JSON.parse(data);
console.log(chatList); //getting only empty array everytime
};
useEffect(() => {
sk.on('chatlist', onChatListReceived);
sk.on('chatlistToUpdate', onChatListToUpdateReceived);
return () => {
sk.off('chatlistToUpdate');
sk.off('chatlist');
};
}, []);
// Emit chatlistToUpdate whenever chatList is updated
useEffect(() => {
sk.emit('chatlistToUpdate');
}, [chatList]);
return {
chatList,
end
};
};
I'm new to react native. I have a problem while updating data(latitude, longitude, timestamp) for each user(member) to Firebase database realtime, I get endless loops
The error I have "too much recursion"
firebase.config.ts
const firebaseConfig = {
...
};
export const getCurrentLocation = (phoneNumber: number, setData: (locationParams: any) => void) => {
const db = getDatabase();
const reference = ref(db, 'members/' + phoneNumber);
onValue(reference, (snapshot) => {
const data = snapshot.val();
setData(data);
})
};
export const setCurrentLocation = (phoneNumber: number, latitude: number, longitude: number, timestamp: number) => {
const db = getDatabase();
const reference = ref(db, 'members/' + phoneNumber);
set(reference, {
latitude: latitude,
longitude: longitude,
timestamp: timestamp,
}).then(() => console.log('setCurrentLocation to mainUser/firebase.config'));
};
const app = initializeApp(firebaseConfig);
memberList.tsx
const [userPhoneNumber, setUserPhoneNumber] = useState('0');
const members = useSelector((state: any) => state.members);
//get user's phoneNumber
const auth = getAuth();
useEffect( ()=> {
onAuthStateChanged(auth, (user) => {
if (user) {
setUserPhoneNumber(user.email.replace(/\D/gi, '') || '');
} else {
console.log('logged out')
}
});
}, [])
useEffect(() => {
const timer = setInterval( () => {
members.map(function (el) {
getCurrentLocation(el.phoneNumber, (locationParams: any) => {
let timestamp = new Date().getTime()/1000;
setCurrentLocation(el.phoneNumber, locationParams.latitude, locationParams.longitude, timestamp)
})
})
}, 10000);
return () => clearInterval(timer);
}, []);
This looks wrong:
const timer = setInterval( () => {
members.map(function (el) {
getCurrentLocation(el.phoneNumber, (locationParams: any) => {
let timestamp = new Date().getTime()/1000;
setCurrentLocation(el.phoneNumber, locationParams.latitude, locationParams.longitude, timestamp)
})
})
}, 10000);
Since your getCurrentLocation attaches a listener with onValue, just doing that once for each user will execute your setData call right away for the current value in the database, and again whenever that user's data is updated. Combining onValue with an interval timer is an antipattern in Firebase, the database already calls back to your code when the data changes. Right now you attach a new listener for each user each second, so if there's an update to a user's data after more than a few seconds, your callback (and setData) will be called way more often than necessary.
In Firebase, attach your listeners once (typically when the page loads) and let them continue to work until the page is unloaded (or other changes are needed in the listeners).
So if you remove the setInterval from the code, you should be getting better results.
I am currently making a Spotify clone which gives user a preview of the song. The problem occurs when I am making many different api requests. When there are more than one requests on the page, it throws a 429 error(making too many requests at once).
Please read through the whole question as I have mentioned the steps I have taken to fix this below.
Profile.js
const { api, refreshableCall } = useSpotify()
const [error, setError] = useState(null)
const [userName, setUserName] = useState("")
const [userFollowers, setUserFollowers] = useState("")
const [userImage, setUserImage] = useState([])
const [userLink, setUserLink] = useState("")
const [userId, setUserId] = useState("")
const [userFollowing, setUserFollowing] = useState("")
const [userTopArtists, setUserTopArtists] = useState([])
const [userTopSongs, setUserTopSongs] = useState([])
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks({
limit: 10,
time_range: "long_term"
}))
.then((res) => {
if (disposed) return
setUserTopSongs(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setUserTopSongs([])
setError(err)
});
return () => disposed = true
})
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMe())
.then((res) => {
if (disposed) return
var data = res.body
setUserName(data.display_name)
setUserImage(data.images)
setUserFollowers(data.followers["total"])
setUserLink(data.external_urls.spotify)
setUserId(data.id)
setError(null)
})
.catch((err) => {
if (disposed) return
setUserName("")
setUserImage([])
setUserFollowers("")
setUserLink("")
setUserId("")
setError(err)
});
return () => disposed = true
})
useEffect(() => {
let disposed = false
refreshableCall(() => api.getFollowedArtists())
.then((res) => {
if (disposed) return
var data = res.body
var artists = data.artists
setUserFollowing(artists.total)
})
.catch((err) => {
if (disposed) return
setUserFollowing([])
setError(err)
});
return () => disposed = true
})
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopArtists({
limit: 10,
time_range: "long_term"
}))
.then((res) => {
if (disposed) return
var data = res.body
var artists = data.items
setUserTopArtists(artists)
setError(null)
})
.catch((err) => {
if (disposed) return
setUserTopArtists([])
setError(err)
});
return () => disposed = true
})
SpotifyContext.js
import React, { useState, useEffect, useContext } from "react"
import axios from "axios"
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 1234567890,
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => { throw new Error("context not loaded") },
refreshAccessToken: () => { throw new Error("context not loaded") },
hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({ children }) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return;
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
const refreshToken = tokenInfo.refreshToken;
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Errors
Without the dependency, it keeps cycling and firing off requests, likely hundreds per second. (Error 429)
With the dependency, it seems the Access Token is being ignored or sidestepped. (Error: WebApiAuthentication - No token provided)
What I have tried to do ?
I tried to implement all the requests in a single useEffect, still getting the errors.
Calling useEffect with dependency array and without.
Link to the Github Repo
https://github.com/amoghkapoor/spotify-clone
status 429 means you have made too many calls in a specific time window.
you are therefore banned for this specific time window.
try waiting a bit before retrying.
did you try :
useEffect(..., [])
this guaranties it will be run only once.
None of your useEffect calls are using a dependency array, remember if useEffect is called without any dependencies it goes into an infinite loop. Either find what dependency or state change should re-run the useEffect hook and include it in the dependency array:
useEffect(() => { /* your logic */ }, [dependencies])
or if there are no dependencies simply fire it once the component mounts:
useEffect(() => { /* your logic */ }, [])
I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);
Recently started using Hooks and, as cook as they are, they are giving me a bit of a headache.
I have a custom useFetch() hook that deals with fetching data from the API.
I also have a component where I need to use useFetch a few times and the results must be passed from one to another.
E.g.:
const ComponentName = () => {
const { responseUserInfo } = useFetch('/userinfo')
const { responseOrders } = useFetch(`/orders?id=${responseUserInfo.id}`)
const { isOrderRefundable } = useFetch(`/refundable?id={responseOrders.latest.id}`)
return <div>{isOrderRefundable}</div>
}
So, how do I actually "cascade" the hooks without creating 3 intermediate wrappers? Do I have to use HoC?
Your hook could return a callback, that when called does the API call:
const [getUserInfo, { userInfo }] = useFetch('/userinfo');
const [getOrders, { orders }] = useFetch(`/orders`)
const [getOrderRefundable, { isOrderRefundable }] = useFetch(`/refundable`);
useEffect(getUserInfo, []);
useEffect(() => { if(userInfo) getOrders({ id: userInfo.id }); }, [userInfo]);
useEffect(() => { if(orders) getOrderRefundable({ id: /*..*/ }); }, [orders]);
But if you always depend on the whole data being fetched, I'd just use one effect to load them all:
function useAsync(fn, deps) {
const [state, setState] = useState({ loading: true });
useEffect(() => {
setState({ loading: true });
fn().then(result => { setState({ result }); });
}, deps);
return state;
}
// in the component
const { loading, result: { userInfo, orders, isRefundable } } = useAsync(async function() {
const userInfo = await fetch(/*...*/);
const orders = await fetch(/*...*/);
const isRefundable = await fetch(/*...*/);
return { userInfo, orders, isRefundable };
}, []);