Aborting nested fetch requests when component is unmounting - javascript

The following function, when consumed, fetches and returns an array of up to 50 comments of a single post. It first fetches a single post by ID, and this post object has an array of comments ID's, which it will fetch.
My goal is to early abort this task in my React class component using componentWillUnmount, by calling abort on the signal on the class instance. The signal is passed as abortSignal.
The question is, I have a case of nested fetch requests. How should I approach this so I can make sure any on-going fetch requests are aborted when the component is unmounting? As I seee it, passing the signal to the outer fetch won't suffice if this stage was already completed. Should I create another signal inside of this function, and pass to to individual fetch?
const fetchComments = async (type, abortSignal) => {
const res = await fetch(endpoints[`${type}Stories`]);
const post = await res.json();
return Promise.all(post.slice(0, 50).map(async id => {
const url = endpoints.singleStory.replace('[id]', id);
const comment = await fetch(url);
return comment.json();
}));
}

I don't see any reason you can't reuse the abortSignal across all the fetch calls. You probably also want to check its aborted flag after awaiting so you bail proactively:
const fetchComments = async (type, signal) => {
const res = await fetch(endpoints[`${type}Stories`], {signal});
if (signal.aborted) {
return; // Or whatever
}
if (!res.ok) {
throw new Error("HTTP error " + res.status);
}
const post = await res.json();
if (signal.aborted) {
return; // Or whatever
}
return Promise.all(post.slice(0, 50).map(async id => {
// Probably not much point to checking `signal.aborted` here, you *just* checked it above
const url = endpoints.singleStory.replace('[id]', id);
const comment = await fetch(url, {signal});
if (!comment.ok) {
throw new Error("HTTP error " + comment.status);
}
return comment.json();
}));
}
Two notes on that:
I changed abortSignal to signal so I could use shorthand property notation when passing it to fetch, because I'm lazy. :-)
Your code is falling prey to the fetch footgun — you need to check for HTTP success, it only rejects on network error, not HTTP error. I've inserted checks above.
Note: If you can modify the API, I'd strongly recommend making it possible to ask for the 50 comments as part of the initial fetch, or at least to be able to ask for a batch of comments, rather than loading each comment individually with its own HTTP request (although HTTP/2 helps a lot).

Related

onError Authentication with refresh token

In the Apollographql documentation it states:
The onError link can retry a failed operation based on the type of GraphQL error that's returned. For example, when using token-based authentication, you might want to automatically handle re-authentication when the token expires.
This is followed up by their sample code:
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// Apollo Server sets code to UNAUTHENTICATED
// when an AuthenticationError is thrown in a resolver
case "UNAUTHENTICATED":
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// Retry the request, returning the new observable
return forward(operation);
}
}
}
// To retry on network errors, we recommend the RetryLink
// instead of the onError link. This just logs the error.
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
My question is in regards to the getNewToken(), as no code was provided for this function, I want to know (assuming this is another request to the backend and I am not sure how it could not be), if you are able to and or supposed to use query/mutation in graphql or make the request through axios for example.
One problem, if it can/should be a graphql query or mutation, is to get the new token, the onError code is defined in the same file as the ApolloClient as ApolloClient needs access to onError, thus when trying to implement this as retrieving a new token through a graphql mutation I got the following error:
React Hook "useApolloClient" is called in function "refresh" that is
neither a React function component nor a custom React Hook function.
After trying to useQuery/useMutation hook and realizing I cannot outside of a react component and at the top level I found this post whose answers suggested you can use useApolloClient.mutate instead but I still ran into issues. My code was (and tried multiple iterations of this same code like useApolloClient() outside of the function and inside etc.):
const refresh = () => {
const client = useApolloClient();
const refreshFunc = () => {
client
.mutate({ mutation: GET_NEW_TOKEN })
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
};
refreshFunc();
};
I could capitalize Refresh but this still would not work and would break the rules of hooks.
And to clarify all the above would do is I would replace the console.logs with setting session storage to the retrieved new token and then re trying the original request with onError.
Now in another post I found when looking into this, the users getNewToken request was a rest request using axios:
const getNewToken = async () => {
try {
const { data } = await axios.post(
"https://xxx/api/v2/refresh",
{ token: localStorage.getItem("refreshToken") }
);
localStorage.setItem("refreshToken", data.refresh_token);
return data.access_token;
} catch (error) {
console.log(error);
}
};
Now from my understanding, if I wanted to implement it this way I would have to change my backend to include express as I am only using apolloserver. Now I could definitely be wrong about that as my backend knowledge is quite limited and would love to be corrected their.
So my question is, what is the best way to do this, whether natively using graphql queries/mutations (if possible), doing it with axios, or maybe their is another best practice for this seemingly common task I am unaware of.

async or promise all, reject all if first fails - 3 post method calls

Edited. I need to call addUser() to receive the userId back thats generated from the database. Then i need to call addTicket() with the userId to receive the ticketId back thats generated from the database. Then finally call addToBooks with the userId and ticketId. These are all post methods, the only data I need back from each call are their ids, the last post method will return full data. Is there a way to handle this where I can fail all post methods if any one of the fails in the process? I've seen examples of promise.all that can do this but it was all for get methods.
You could try something like this, following your functions names example:
const getData = async () => {
try {
const userId = await addUser();
if(userId) {
const ticketId = await addTicket(userId);
const response = await addToBooks(userId, ticketId);
return response;
} else {
throw new Error("Error getting data")
}
} catch(error) {
console.log(error);
}
}
This doesn't use Promise.all, but since the example you are giving, to my understanding, only needs the userId to complete the other 2 operations, I think this should work

Data part of Response is a long script instead of desired json object

I am building a web app using laravel and vuejs. I have made a axios get request to get a list of users .
I am getting a Promise object, and from what i have read. Reason for getting a promise object is because it's an async request.
I have tried .then() to get data part of the response. But i am getting a huge script instead of desired data.
axios......then(function(response){
console.log(response.data);
})
Initially what i did was
var res = axios.get('/allUsers');
console.log(res)
That time i came to know about promise object and read about.
When i checked network in dev tools, status code is 200 and i can see list of users. So i guess my request is successfully completed.
What should be done to get the list of the users. That list i will be using to update my UI.
Depending on what you're getting back for data there are a few ways to handle this. You may need to convert the data after the you get receive the response.
axios.get('some_url')
.then(res => res.json())
.then(data => {
// do something with the data
}).catch(err) {
conosole.error(err);
}
if you're seeing the data come through properly in the response and you're getting what you need without doing that then just do
axios.get('some url').then(res => {
// do something in here with the data here
})
also make sure you're getting back json if that's what you're looking for. check your response to see if its html or json because they can be handled a bit differently
as an "Edit" you could also handle this with async await so you dont end up in callback hell
async function fetchData() {
try {
const res = await axios.get('some url');
// next step might not be necessary
const data = await res.json();
// do something with the data
console.log(data); // if converting it was necessary
console.log(res); // if not converting
} catch (err) {
console.error(err);
}
}

Why doesn't this async function throw undefined?

I have a pretty simple async function that calls the fetch api, and brings me back some data. I am using the await keyword 2 times in this function, and then getting that data and pushing it into my component state.
Here is my pseudo-code in regards to how this function is executing (please tell me if I'm right or wrong here):
Call the fetch api with await: this allows the rest of your code to continue to the next line.
Once you get the fetch the response stream, put it into data variable. Again, the code can continue while we are waiting for this to happen.
Log the data to the console.
Step 3 is where I have some questions...let's say I'm on a really terrible network, and my fetch request doesn't give me my data for 5 full seconds. At that point, shouldn't my console.log(data) line throw undefined, and execute the catch block, due to the async function allowing console.log(data to run BEFORE I get my fetch data back?
I tested this by going into the Chrome Web dev console, and selected the "slow 3g" connection. Even with that network, I was able to log my data to the console without throwing undefined.
What I want to do is to make sure there is data to push into state right after I get my data object back.
getMovieDetails = async id => {
const API_KEY = process.env.REACT_APP_API_KEY;
const movieId = id;
const url = `https://api.themoviedb.org/3/movie/${movieId}?api_key=${API_KEY}&language=en-US`;
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
this.setState({
title: data.title,
poster: data.poster_path
});
} catch (err) {
console.log(`Couldn't fetch the endpoint!`);
console.log(err);
}
};
You are wrong in the first point of your Pseudo-code
Call the fetch api with await: this allows the rest of your code to
continue to the next line.
Actually, no. await will block the execution of the next lines in the async function.
So,
yourAsyncFunction = async () => {
await doSomething();
console.log('done something') // Will not run until doSomething() gets completed
}
Which is why you are always getting the fetched data in your console.log(data) statement.

Firebase: Calling Cloud Function From Cloud Function

I am running in to an issue with Firebase cloud functions. I have an onWrite cloud function that triggers a sequence of events. I have a path for requests that the onWrite cloud function is tied to. When that cloud function executes, it deletes the new request for the requests path and pushes the request in to a render path/que that will be used client side for rendering UI elements/data. Once the data has been written to the render path, I call a vanilla javascript function that is not tied to any cloud events. The vanilla javascript function is supposed to reach out to an external API and fetch some data to later be updated on the render object that was pushed in to the render path.
The problem is that the vanilla javascript function never executes. I have been looking all over the web to figure out why this happening but can't seem to figure out why. I am on the Flame plan so outbound api requests should be allowed to my knowledge. Here an example of my code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const request = require('request');
admin.initializeApp();
exports.requestModule = functions.database.ref('/requests').onWrite((change, context) => {
// Create reference to database
let db = admin.database();
if (context && context.auth && context.auth.uid) {
const afterData = change.after.val();
let uid = context.auth.uid;
let cleanData = afterData[uid];
cleanData.status = "loading";
// Remove the requested module from the requests path
let cleansePath = db.ref('/requests/' + uid);
cleansePath.remove().then((snapshot) => {
return true;
}).catch((error) => {
console.log(error);
return false;
});
// Add requested module to the render path
let renderPath = db.ref('/render/' + uid);
renderPath.push(cleanData).then((snapshot) => {
let val = snapshot.val();
let key = snapshot.key;
// Trigger the get weather api call
getWeather(uid, key, val);
return true;
}).catch((error) => {
console.log(error);
return false;
});
}
});
// Fetches data from external api
function getWeather (uid, key, obj) {
console.log('Fetching weather!');
let db = admin.database();
request('https://api.someweathersite.net/forecast/', (error, response, body) => {
if (!error && Number(response.statusCode) === 200) {
console.log('error:', error);
console.log('statusCode:', response && response.statusCode);
console.log('body:', body);
obj.data = body;
obj.status = 'loaded';
// Set data from api response in render object to be shown client side
let render = db.ref('/render/' + uid + '/' + key );
render.set(obj).then(() => {
return true;
}).catch((error) => {
console.log(error)
return false;
});
}
});
}
The console.log message at the top of the "getWeather" function never executes. I don't think that the "getWeather" function is ever executing.
If I put the api call directly in the onWrite "requestModule" function, the api call will work. However, when it calls an external function it never gets called/works. I basically want to have the "requestModule" function handle all requests and plan to have a module dispatcher that handles which module function/api data should be fetched from. That's why I don't want to keep the api call in the "requestModule" function. Any idea of why this happening or how I can get this working?
getWeather is performing asynchronous work to fetch some data, but it's not returning a promise to indicate when that work is complete. In fact, none of the async work you're performing here is correctly using the promises returned by the various API calls. It's not sufficient to simply use then() on each promise.
You need to keep track of all of the async work, and return a single promise that resolves only after all the work is complete. Otherwise, Cloud Functions may terminate and clean up your function before the work is complete. (Note that it's not deterministic which work may or may not actually complete before forced termination, but the only way to ensure that all work completes is through that single promise you return.)
You may want to watch my tutorials on using promises in Cloud Functions to get a better handle on what you're required to do make your functions work correctly: https://firebase.google.com/docs/functions/video-series/

Categories

Resources