I have a React application and I have a centralized API Class. So All of the API calls I make, go through APIClass. I call a specific function of an API class with props and that fetches the data using APICaller and returns it asynchronously. The problem is that I need to implement axios cancel token for it and not sure how could I achieve it. In the below setup, callAPIone would return the data while callAPItwo would fail. How can I make it so second call would cancel first one?
let i = await this.apiClass.callAPIone(1);
let y = await this.apiClass.callAPIone(2,cancel);
Then within apiClass, I have functions like
callAPIone = async (param,cancel) => {
return await ApiCaller(param,cancel)
}
Then within ApiCaller I have
async function ApiCaller(param,apiCancel) {
let source = axios.CancelToken.source();
if (apiCancel) source.cancel("Request canceled.");
let params = param
return await axios
.get(apiRoute, { headers, params, cancelToken: source.token })
.then((response) => OnSuccess(response))
.catch((e) => OnError(e.message));
}
Related
I have an API called getQuote and a component called QuoteCard. Inside QuoteCard I'm trying to render an array of users that liked a quote. The API works fine, I have tested it, and the code below for getting the users works fine too.
const Post = async (url, body) => {
let res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"accept": "*/*"
},
body: JSON.stringify(body)
}).then(r => r.json());
return res;
}
const getAllLikes = async () => {
let users = await Post('api/getQuote', {
id: "639e3aff914d4c4f65418a1b"
})
return users
}
console.log(getAllLikes())
The result is working as expected :
However, when trying to map this promise result array to render it onto the page is where I have problems. I try to render like this:
<div>
{getAllLikes().map((user) => (
<p>{user}</p>
))}
</div>
However, I get an error that states:
getAllLikes(...).map is not a function
I don't understand why this is happening. Why can't I map the array? Is it because it's a promise or something?
And if anyone needs to see the getQuote API, here it is:
//Look ma I wrote an API by myself! :D
import clientPromise from "../../lib/mongodb";
const ObjectId = require('mongodb').ObjectId;
import nc from "next-connect";
const app = nc()
app.post(async function getQuote(req, res) {
const client = await clientPromise;
const db = client.db("the-quotes-place");
try {
let quote = await db.collection('quotes').findOne({
_id: new ObjectId(req.body.id)
})
res.status(200).json(JSON.parse(JSON.stringify(quote.likes.by)));
} catch (e) {
res.status(500).json({
message: "Error getting quote",
success: false
})
console.error(e);
}
})
export default app
Thanks for any help!
It is due to the fact that getAllLikes is an async function and thus it returns promise which does not have a map function.
You can either save it in a state variable before using await Or chain it with .then.
Minimal reproducible example which works
const getAllLikes = async () => {
return ['a', 'b']
}
getAllLikes().then((r) => r.map((g) => { console.log(g) }))
Edit: The above code won't work if directly used with jsx since the return of getAllLikes will still be a promise. Solution would be to save it in a state variable and then using it.
I am from Angular and I believe we call pipe on Observables (or Promises). Map can then be called inside the pipe function
observable$ = getAllLikes().pipe(map( user => <p>{user}</p>))
If there is no pipe, I can only think of manually subscribing (which is not a good practice)
sub$ = getAllLikes().subscribe( user => <p>{user}</p>)
// unsub from sub$ appropriately
// We do this from ngOnDestroy in angular
ngOnDestroy() {
this.sub$?.unsubscribe()
}
I have two async functions which reach out to API endpoints (Serverless Framework) - one gets and returns a token, the other gets and returns data using the token.
I'm testing these by using simple buttons, where onClick calls the functions to pull the token and the data, respectively. Click one button to get the token, which is saved to state. Then, once I see the token has been received, I click the other button to get the data. This works without any issues at all.
The problem is when I try calling them sequentially from the React app. I need to call these back-to-back when the user submits a request. I can't seem to make the code wait for the token to arrive before trying to pull the data.
The functions being called in the onClick method of the button:
const tokenBtnOnClick = () =>
{
const response = getToken().then(x => {
setToken(x.data.response.token)
})
}
const dataBtnOnClick = () =>
{
const response = getData(token, param1, param2, param3).then(x => {
setData(x.data.response)
})
}
Async functions:
export async function getToken()
{
const apiUrl = `${BASE_URL}/handler/getToken`
const axios = require('axios').default
let response
try
{
response = await axios.get(apiUrl)
}
catch (e)
{
console.log(e)
}
if (response)
{
return response
}
else
{
return ''
}
}
export async function getData(token, param1, param2, param3)
{
const apiUrl = `${BASE_URL}/handler/getData?token=${token}¶m1=${param1}¶m2=${param2}¶m3=${param3}`
const axios = require('axios').default
let response
try
{
response = await axios.post(apiUrl)
}
catch (e)
{
console.log(e)
}
if (response)
{
return response
}
else
{
return ''
}
}
I've tried calling this getBoth() function in a single button's onClick:
async function getBoth()
{
const tokenResponse = await tokenBtnOnClick().then(x => setToken(x.data.response.token))
const dataResponse = await dataBtnOnClick().then(x => setData(x.data.response))
}
But even though it's an async function that uses await on both lines, I always get the same TypeError because dataBtnOnClick is called immediately, without actually waiting for the token to come in. When I run this code, tokenBtnOnClick is called, the app crashes due to a TypeError, and then the token comes in and is logged and saved to state.
I've also tried this: (where getData is exactly as above, but now accepts token as a paramter rather than using the state variable)
async function getBoth()
{
const response = await getToken().then(x => getData(x.data.response.token))
}
index.js?bee7:59 Uncaught (in promise) TypeError: Cannot read
properties of undefined (reading 'then')
How do I get this to actually wait for the token to come in before trying to pull the data?
You are calling setToken and expection token to be updated immediately, but setToken will be asynchronously applied.
Can you useEffect to solve your problem?
useEffect(() => {
getData(token, param1, param2, param3).then(x => {
setData(x.data.response)
})
}, [token])
Try this
const tokenBtnOnClick = () =>{
setToken(getToken())
}
const dataBtnOnClick = () =>{
setData(getData(token, param1, param2, param3))
}
and
const axios = require('axios').default
export async function getToken()
let apiUrl = `${BASE_URL}/handler/getToken`
{
let response = await axios.get(apiUrl)
return response.data.token;
//i don't know exactly what the api returns so it may be diferent
}
export async function getData(token, param1, param2, param3)
{
let apiUrl = `${BASE_URL}/handler/getData? token=${token}¶m1=${param1}¶m2=${param2}¶m3=${param3}`
let response = await axios.post(apiUrl)
return response.data.response;
}
and in your getBoth() just call them because the functions are asynchronous the code will only move forward after them are finished
getBoth(){
setToken(getToken())
setData(getData(token, param1, param2, param3))
}
I need to change a parameters that defines what data should come from my requests, also this application needs to refresh on a regular time interval. If the user changes the parameter in the middle of an unfinished request things start to behave strange and some unexpected behavior occurs.
So my approach was to abort all previous requests before starting the new ones, but after using await controller.abort() it seems that the next requests are never triggered, Do I need to clear the signal or something like that?
const controller = new AbortController();
const async fetchData = (url, body = null) => {
let data;
const signal = controller.signal;
const headers = { ... };
response = await fetch(url, body ? {
method: "POST",
body: JSON.stringify(body),
signal,
headers
} : { headers, signal });;
data = await response.json()
return data
}
const firstData = await fetchData(url1, body1);
await controller.abort();
const secondData= await fetchData(url2, body2);
What happens is that secondData always is undefined, actually this second request never happens (looking on network traffic). If I stop source and try to run await fetchData(url2) after .abort() has executed it prompts an erros saying that Uncaught SyntaxError: await is only valid in async function or if I try to run it without await it returns a pending promise, but the actual request is nowhere to be seen in traffic tab.
Solved
Applying what was suggested on the ansewr I created wrapper on the function, to call new controllers everytime.
let controller = null;
let fetchData = null;
const initializeFetchData = () => {
const controller = new AbortController();
const async fetchData = (url, body = null) => {
let data;
const signal = controller.signal;
const headers = { ... };
response = await fetch(url, body ? {
method: "POST",
body: JSON.stringify(body),
signal,
headers
} : { headers, signal });;
data = await response.json()
return data
}
}
initializeFetchData();
const firstData = await fetchData(url1, body1);
controller.abort();
initializeFetchData();
const secondData= await fetchData(url2, body2);
You are using the sameAbortController for two different requests. After calling .abort() on theAbortController you have updated the state of it's AbortSignal which then renders the second request void.
You should use a separate AbortController for each request if you want this behavior. Of course, it is perfectly acceptable to reuse an AbortController for multiple fetch requests if you want to be able to abort all of them in one go.
A couple of other points...
.abort() is a synchronous method which returns void so you do not need the await prefix when calling .abort().
In your code example, the first request will never be aborted as you are awaiting the fetch request, which will complete before the .abort() is called.
I wanted to use a function as a react hook to wrap fetch requests to an API.
My current hook:
export function useAPI(url, options={}) {
const [auth, setAuth] = useGlobal('auth');
const [call, setCall] = useState(undefined);
const apiFetch = async () => {
const res = await fetch(url, {
...options,
});
if (!res.ok)
throw await res.json();
return await res.json();
};
function fetchFunction() {
fetch(url, {
...options,
});
}
useEffect(() => {
// Only set function if undefined, to prevent setting unnecessarily
if (call === undefined) {
setCall(fetchFunction);
//setCall(apiFetch);
}
}, [auth]);
return call
}
That way, in a react function, I could do the following...
export default function LayoutDash(props) {
const fetchData = useAPI('/api/groups/mine/'); // should return a function
useEffect(() => {
fetchData(); // call API on mount
}, []);
render(...stuff);
}
But it seems react isn't able to use functions in hooks like that. If I set call to fetchFunction, it returns undefined. If I set it to apiFetch, it executes and returns a promise instead of a function that I can call when I want to in the other component.
I initially went for react hooks because I can't use useGlobal outside react components/hooks. And I would need to have access to the reactn global variable auth to check if the access token is expired.
So what would be the best way to go about this? The end goal is being able to pass (url, options) to a function that will be a wrapper to a fetch request. (It checks if auth.access is expired, and if so, obtains a new access token first, then does the api call, otherwise it just does the API call). If there's another way I should go about this other than react hooks, I'd like to know.
Instead of putting your function into useState, consider using useCallback. Your code would look something like this:
export function useAPI(url, options={}) {
const [auth, setAuth] = useGlobal('auth');
function fetchFunction() {
fetch(url, {
...options,
});
}
const call = useCallback(fetchFunction, [auth]);
const apiFetch = async () => {
const res = await fetch(url, {
...options,
});
if (!res.ok)
throw await res.json();
return await res.json();
};
return call
}
The returned function is recreated whenever auth changes, therefore somewhat mimicking what you tried to do with useEffect
I'm fetching data on componentDidMount in fetchData method. After that I'm trying to delete data with the method. I was trying to update date immediately in deleteUser method, but it doesn't work. How can I update this data after fetch with delete method?
fetchData = () => {
let url = `https://contact-browser.herokuapp.com/contacts`;
fetch(url, {method: 'get'}).then(resp => resp.json()).then((data) =>
this.setState({
data: Object.entries(data).map(([key,value])=>({key:Number(key),value}))
})
);
};
componentDidMount() {
this.fetchData();
}
deleteUser = (id) => {
let url = `https://contact-browser.herokuapp.com/contact/${id}`;
fetch(url, {method: 'delete'}).then(resp => console.log(resp));
this.fetchData();
};
Refetch your data after the delete fetch has fulfilled, this ensures that the data is fetched after the delete has fully resolved on the server.
deleteUser = (id) => {
let url = `https://contact-browser.herokuapp.com/contact/${id}`;
fetch(url, {method: 'delete'}).then(resp => {
this.fetchData();
});
};
It "did not work" because of the async behaviour of JS.
To be simple, when there is any waits in the code (like DB Call, API Call, etc), the JS code lets that particular to run and starts executing the next line. More on this - https://www.hongkiat.com/blog/synchronous-asynchronous-javascript/
In this case, since fetch is an i/o wait, the control moves to the next line - this.fetchData(). So, the fetchData() is called before delete may actually happens.
Following is another solution using async and await. More to read
deleteUser = async (id) => {
let url = `https://contact-browser.herokuapp.com/contact/${id}`;
let resp = await fetch(url, {method: 'delete'});
console.log(resp);
this.fetchData();
};
You can also use async and await
deleteUser = async (id) => {
let url = `https://contact-browser.herokuapp.com/contact/${id}`;
const deleted = await fetch(url, {method: 'delete'}).then(resp => console.log(resp));
if(deleted){
await this.fetchData();
}
};