Fetching nested async data to fill a React component - reactjs

Hoping to get some guidance on how to best work with async data calls in React.
I have a page that has a FullCalendar component which will show the events given to it in a state, i.e., simplifying a lot,
return (
...
<FullCalendar events={ events } />
)
What I need to do to get the events is first to make a call to Google Calendar's APIs to fetch the Oauthed user's calendars, then looping through each calendar, make a call to Google calendar to fetch that calendar's events. Something like (very simplified):
let eventsToShow = [];
apiCalendar.listCalendars({}).then(({result}) => {
// At this point I have the list of the user's calendars
result.items.forEach((calendar) => {
// Now I have all the events on a given calendar
apiCalendar.listEvents(calendar.id).then(({result}) => {
eventsToShow.push({id: result.id, start: result.start, end: result.end});
})
})
})
setEvents(eventsToShow);
Right now, I have a button on the page, and I'd like to kick off the calls to fetch the calendars and events when I click the button (because I have a separate button that does the Oauth sequence). Later, my intent is to store something from the Oauth so that the page will just pull the calendars and events automatically if the token is present.
In the above pseudocode, I have various pieces working - the FullCalendar component can show hard-coded events, my various API calls do fetch the correct date - but I'm having trouble figuring out how to put it all together.
I suspect I have to use a useEffect as well as use await, but the nested nature of the API calls is confusing me. If I do have to use useEffect, I'm also not sure how to get the calls to trigger off a button click for now.
I've tried having an onClick function for the button that just leads to essentially the second block of pseudocode above. This worked for causing the API calls to run (as evidenced by console.logs along the way), but the setEvents part at the end doesn't work, I suspect because of the async nature of things.
Help? Am I completely misguided?

If what you want is to bind the api calls to the click of the button you wouldn't need a useEffect.
Definitely you can improve the readability of the async calls with the use of async await. From what you shared I get that the async call should be something like this:
const fetchCalendarEvents = async()=>{
try{
const calendars = await apiCalendar.listCalendars()
const events = await Promise.allSettled(calendars.map(c=>{
return apiCalendar.listEvents(c.id)
}))
return events
}
catch (error){
console.error(error)
}
}
You can add the required parameters that you need to do the api call.
Within the component then you can define the onClick event on the button which will trigger this function something like
const handleEventsFetch = async()=>{
const events = await fetchCalendarEventts(
//...params
)
setEvents(events)
}
If you ever want the fetching to be on component load you can definitely do that in a useEffect. Note you cant use async-await in useEffect but we can use then/catch or an IIFE
//then
useEffect(()=>{
fetchCalendarEvents(
//...
).then(
setEvents
)
},[])
//iife
useEffect(()=>{
(async()=>{
const events = await fetchCalendarEvents(
//...
)
setEvents(events)
})()
},[])
Note I don't know what the params for the api calls are so beware that you might need to add dependencies to the useEffect. In consequence of that you might also need to add some if checks before running the api call so that you don't run it multiple times if any of the dependencies change
EDIT: Add transformation on returning the events
const fetchCalendarEvents = async () => {
try {
const calendars = await apiCalendar.listCalendars();
//rename this so that it is more descriptive
const calendarsWithEvents = await Promise.allSettled(
//make the map function async!
calendars.map(async (c) => {
// now you can use await
const calEvents = await apiCalendar(c.id);
return {
calendarId: c.id,
events: calEvents,
};
})
);
return calendarsWithEvents;
} catch (error) {
console.error(error);
}
};

Related

React change state automatically after a post has been added on other component

I want to create a ticket functionality. On the main page (the parent) I have a axios get function that once the page is loaded, the useEffect is listing all the tickets components.
On the second page I have the child component where I imported with props the reloadTickets={fetchTickets} method to be used every time I have added a new ticket.
The problem is that If I add a post, one time the post is not added, second time the post is added twice and third time the post is adding too many times on a single form submission. Sometime the post is not added at all, but after I try more times, the post is added twice and more.....
This is my PARENT COMPOENTN
This is my Child component
PS. Sorry for the pictures. But react code cannot be formated to look good on my page.
Well the problem is you are not waiting for axios call to finish, since axios.post method is asynchronous.
You need to make your function async and await axios.post method, here is an example how you can do this.
const createTicket = async (e) => {
e.preventDefault();
try {
await axios.post(postTicketURL, {
userID: userID,
content: ticketArea
}, configHeader);
await reloadTickets();
}
catch (error) {
console.log(error);
}
}
Update: I see that you are already using .then and .catch approach in your parent component, so you can solve this with that like this:
const createTicket = async (e: any) => {
e.preventDefault();
axios.post(postTicketURL, {
userID: userID,
content: ticketArea
}, configHeader).then((res: any) => {
console.log(res);
reloadTickets();
}).catch((err: any) => {
console.log(err);
});
}
But I would recommend async await approach, and also you should refactor your code inside TicketsComponent -> fetchTickets to match that.
Why?
Because async await is much more clear and modern approach and you are avoiding potential callback hell, for example if you now want to wait reloadTickets function to end properly. You can find out more about this topics here:
https://www.loginradius.com/blog/async/callback-vs-promises-vs-async-await/

What is the best way to execute a function after TWO or more async functions finish in React?

I have two functions that run asynchronously getting data from the API. Both of them are called from their own useEffect().
I have a third function that needs to run once those two functions have been fully completed.
How can this be accomplished?
Edit:
Both of the async functions look like this:
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
const fetchBudgetsData = async () => {
setIsFetchingBudgets(true);
const res = await getBudgets(orgID, `${parseInt(fiscalYear)}`, '', budgetType);
setIsFetchingBudgets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgets(res.budgets);
};
const fetchBudgetBucketsData = async () => {
setIsLoadingBudgetBuckets(true);
if (orgID === undefined) {
return;
}
const res = await getBudgetBuckets(orgID, fiscalYear);
setIsLoadingBudgetBuckets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgetBuckets(res.buckets);
};
Whenever the budget data or bucket data is updated, I want to call another function that checks for errors. However when the page loads, I need it to wait for both of those functions to be finished before it checks for errors.
Edit #2:
After some debugging, it looks like the issue might have to do with when React updates the state. Since I am trying to check for errors in data saved in the state.
One way could be chaining Promises.
Promise.all([ApiCall1, ApiCall2])
// At this point two promises above will be resolved
.then(() => ApiCall3)
Read more
I discovered the issue was caused by how React chooses when to update the state and not how I was calling these functions asynchronously.
I was able to call my Error check function by hooking it into the output of the data fetch calls. This makes sure that the error check only runs when either the budgets or buckets are edited and finished being changed.
useEffect(() => {
getWarningsAndErrors();
}, [budgets, budgetBuckets]) //Update errors whenever we edit budgets or buckets

How to use { useQuery } from react-query while iterating over an array of data

I'm trying to integrate react-query into my React project.
What I have is a custom hook - useBundles which fetches data from a GraphQl endpoint as shown below -
function useBundles() {
const { data, status, isSuccess } = useQuery(
'SaleBundles',
async () => await request(endpoint, gql`query {${saleQuery}}`),
);
return { data, status, isSuccess };
}
I use the return value in my component like this const { data, status, isSuccess } = useBundles(); which works perfectly fine.
Now, what I want to do is, for each item in data I want to call another endpoint (a REST endpoint this time) and I have a seperate async function for that called getData(data: Array) which uses async-await to fetch data from my REST endpoint.
I could just call getData in useBundles passing to it data as an argument. But since getData is async it is required to use await with it (which I can't because I can't define a hook as async).
As an alternative, I tried not to use getData in useBundles but directly call the endpoint using axios like this -
data.forEach((item) => useQuery("some-unique-key", axios.get(<endpoint>)))
This gives me an error saying that useQuery cannot be used in a loop.
I'm kinda stuck at this point as how to proceed. Would appreciate any help. Thanks.
There are basically two ways to solve this:
useQueries
useQueries can be used with a fixed array, but also with a dynamic one, so:
useQueries(data.map(item => ...))
would work. It will give you an array of QueryResults back.
map and create a component
data.map(item => <Item {...item} />
then, you can call useQuery inside the Item component.
Both approaches will yield concurrent requests, the difference is mainly where you have access to the response data: With useQueries, you have access to all of them in your root component. With the second approach, each Item will only have access to its own data. So it just depends on your needs / use-case which version you prefer.

React multiple http calls when navigating the application using the URL

I have a component which has filtering, searching and pagination capabilities. What I'm trying is to create a queryString and attach to the URL such that I can later copy and paste it in another browser so that I can reuse the filters.
To extract the query params from the URL I'm using the useLocation hook
const useQuery = () => new URLSearchParams(useLocation().search);
const pageNo = useQuery().get('page') ?? 1;
I'm using the useEffect hook to track for changes of the page query parameter value, and dispatch an action which will update the pageNo in the state object of my reducer.
React.useEffect(() => {
dispatch({
type: actionDescriptor.CHANGE_PAGE,
payload: pageNo
});
}, [pageNo]);
I have another useEffect hook which handles the fetch of the data, and gets triggered when the pageNo changes. I'm using the useNavigate to create and navigate to the new location if the http call was successful
const nav = useNavigate();
React.useEffect(() => {
(async function get() {
const response = // make http call and get response
if (response.status === 200) {
dispatch({
type: actionDescriptor.FETCH_SUCCESS,
payload: {
data: response.data['value'],
}
});
nav (`/data?page=${state.pageNo}`);
}
/// handle error
}
})();
}, [state.pageNo, state.pageSize, state.filter]);
When I'm navigating using the UI, selecting a different page for example, all works well, there is a single http call, the browser url is updated as expected (e.g. localhost/mydata?page=2). If however I'm copying the url and paste it in another window, it makes two http calls, and renders the dom twice. What might be the cause for this?
my guess is due to the parameters you are listening on state.pageNo, state.pageSize, state.filter. I'm assuming all of these are null/empty at the beginning of your app. Once you copied and pasted the url, two of these parameters will change which will cause the useEffect to be called twice.
put in a console.log in the useEffect to confirm that. Once that's confirmed, I would re-examine the list of parameters to see if you need to listen to all of them.
I would take a look at the pageNo. It looks like it might be changing from default value to 2 since you have 2 useEffects probably firing for the same render.

React GraphQL query results as an object without JSX?

I am coming here as a last resort! I have been searching for 1-2 weeks now and haven’t came across anything!
I want to use GraphQL in react (I have an Apollo react client and Apollo backend), but I want to be able to call a query and return the results as an object without it needing to be done using a component or class.
I have a tried using client.query but whatever way I try I only get promises returned.
I want something like this:
const myData = some kind of query call
edit:
Want I want to do is pass an array of objects into a component which will then render the data how I want it.
An example would be passing a list of events into a calendar cell to be displayed for that day.
Here's a better example of what I want to do:
const CustomCellOne = () => (
<Query query={my_query} variables={ vars }>
{({ loading, error, data }) => {
const dataPoints = data.blahQuery;
return (
<CustomCellTwo data={dataPoints} />
)
}}
</Query>
);
Network requests are asynchronous to prevent blocking UI. The Fetch API, which Apollo uses under the hood, utilizes Promises instead of the more traditional callbacks to handle asynchronously requesting resources across the network, so Apollo's API is based on Promises as well. There is no method available that will allow you to synchronously fetch a GraphQL query -- you just need to structure your code appropriately to handle the Promise once it resolves.
The following examples assume you've correctly configured a client instance. Using then:
function doSomething () {
client.query({ query: SOME_QUERY })
.then(({ data }) => {
// Use the returned data however you like
})
.catch(error => {
// Handle error
})
}
Using async/await:
async function doSomething () {
try {
const { data } = await client.query({ query: SOME_QUERY })
} catch (error) {
// Handle error
}
}
If you are unfamiliar with Promises, it would be to your benefit to review the appropriate documentation and check out some tutorials.

Resources