Async Await is not working as expected: ReactJS - javascript

I have this problem where in, user must be redirected to respective dashboards on successful log-in. Say user has accounts in two profiles "p1" and "p2" . After Sign-In success, I am making fetch API to see if user has entries the corresponding profiles.
1)Say if a user has entries in p1, I need to redirect him to "p1" dashboard ;
2) if entries in p2, then redirect him to "p2" dashboard.
3)If no entries are present neither in p1 or p2, then redirect to configuration page where he can make some entries.
4) If in both profile, then user will be asked to select a respective dashboard
Now In my case, the code that I have written is not working as expected. Even though I have no accounts in "p2" but have accounts in "p1" its taking me to configuration page. Can someone help what is wrong in this code?
Note that fetch call just works fine! It gives me array of items. If no items present it returns an empty array
// On login
handleLogin = async () => {
// after successful signin
await Promise.all([
this.setDashboard("p1"),
this.setDashboard("p2"),
]);
this.navigateToDashboards();
};
setDashboard = async (profile) => {
let response, value;
let type = profile;
if (type === "p1") {
response = await this.fetchUserAccounts(type);
value = !!response && response.length ? true : false;
this.setState({ isP1: value });
} else {
response = await this.fetchUserAccounts(type);
value = !!response && response.length ? true : false;
this.setState({ isP2: value });
}
};
fetchUserAccounts = async (type) => {
try {
let response = await fetch({
url: "/fetch/entries",
method: "POST",
body: {
profile: type,
}
);
return response.json();
} catch (error) {
console.log(error);
}
};
navigateToDashboards = () => {
let { isP1, isP2 } = this.state;
if (isP1 && isP2) {
// CODE FOR PROMPTING WHICH DASHBOARD TO GO
} else if (!isP1 && !isP2) {
this.props.history.push("/configpage");
} else if (isP1) {
this.props.history.push("/p1dashboard");
} else if (isP2) {
this.props.history.push("/p2dashboard");
}
};

There are some issues with the code you wrote above and they probably go deeper than we can see in the snippet – why having the logic for the dashboard type on the client-side instead of having it sent from the server?
There is also cloudType that is not specified in the setDashboard method.
I do not know what you are trying to achieve so I'm guessing – you should probably use fetchUserAccounts inside componentDidMount and save the response in the state.
I've came up with something like this with one TODO: in the code:
class X extends React.Component {
constructor(props) {
super(props);
this.state = {
p1: null,
p2: null,
};
}
async componentDidMount() {
const [p1, p2] = await Promise.all([
this.fetchUserAccounts('p1'),
this.fetchUserAccounts('p1'),
]);
// NOTE: setState has a second parameter, which is a callback – this way you make sure that the state has been updated – read more here:
// https://reactjs.org/docs/react-component.html#setstate
this.setState(s => ({ ...s, p1, p2 }), this.navigateToDashboards);
}
fetchUserAccounts = async (type) => {
try {
let response = await fetch({
url: "/fetch/entries",
method: "POST",
body: {
profile: type,
},
});
return response.json();
} catch (error) {
console.log(error);
}
};
navigateToDashboards = () => {
const { history } = this.props;
const { p1, p2 } = this.state;
// TODO: Implement your logic how you decide
if (isP1 && isP2) {
// CODE FOR PROMPTING WHICH DASHBOARD TO GO
return;
}
if (isP1) return history.push('/p1dashboard');
if (isP2) return history.push('/p2dashboard');
return history.push('/configpage');
}
}
If this does not work at least have a look at the code structure. There are places where you do not require the else statement. Use the function version of the setState. Make use of object/array desctrucuring. You should also probably read about what is false and true in the JS world based on the !!response && response.length ? true : false; lines – or maybe there is something I'm missing out here.

The problem is you're using async / await wrong.
await works on promises- you give it a promise, and it waits for it to resolve / reject. If you do not give it a promise- its like passing it an immediately resolved promise.
Your setDashboard function does not reutrn a promise, so what happens is that this code:
await Promise.all([
this.setDashboard("p1"),
this.setDashboard("p2"),
]);
actually resolves immediately, but the setDashboard functions are not executed up until this.navigateToDashboards();. why? because they are defined as async, which puts them in event loop. Then comes in the await, but it immediately resolves as the functions inside does not return a promise (well, it does, because it is async, but its like writing return promise.resolve() as the first line- so the await does nothing only placing the function in event loop)- and the code continues to execute in synchronous fashion until it reaches the end of the function (e.g- executing this.navigateToDashboards();, and only then goes to event loop and execute the setDashboards functions.
On the same subject, response = await this.fetchUserAccounts(type); line also has redundant await, because fetchUserAccounts() does not return a promise.
To correctly use async / await- make sure your async function return a promise, for example:
setDashboard = async (profile) => {
return new Promise( resolve, reject => {
let response, value;
let type = profile;
if (type === "p1") {
response = await this.fetchUserAccounts(type);
value = !!response && response.length ? true : false;
this.setState({ isP1: value });
resolve();
} else {
response = await this.fetchUserAccounts(cloudType);
value = !!response && response.length ? true : false;
this.setState({ isP2: value });
resolve();
}
})
};
as for fetchUserAccount:
fetchUserAccounts = async (type) => {
return new Promise( resolve, reject => {
try {
let response = await fetch({
url: "/fetch/entries",
method: "POST",
body: {
profile: type,
}
);
resolve(response.json());
} catch (error) {
console.log(error);
reject(error);
}
}
};
Notice that you will now have to handle promise rejections using .catch, otherwise you'll get unhandled promise rejection error.

Related

Proper way of dealing with returning from a fetch

I'm a bit new to coding in a JS/TS way and I just wanted to know the best practice when it comes to returning and handling void and values that come from a fetch. In short, I'd like to use fetch to retrieve a JSON file somewhere, extract some relevant info, and return a dict.
async function doSomething() {
const response = fetch(...)
.then(response =>
response.json().then(data => ({
data: data,
response: response.ok
}))
)
.then(data => {
if (!data.response) {
console.warn("Response was not ok...")
return null
} else if (data.data["results"] == 0) {
console.info("Returned no results")
return null
} else {
const returned = {
name: data.data["stuff"],
thing: data.data["other"]
}
return returned
}
})
.catch(error => console.error(`${error}`))
return response
TS returns an error saying that Property 'name' does not exist on type 'void | {...} when I want to use the values of the dict. The error, I completely understand. When using the function I try to do type checking to handle when it returns void, but it still raises an error for something like this:
const request = await getQuery("movie", query)
if (request === undefined || request === null) return "I wasn't able to find your query."
const data = {
name: request.name
}
}
I'm just wondering what's the best way to write and handle void, and using the dict values.
I wouldn't have the function do too much. Have it concentrate on returning some data, and let the function that calls it deal with how that data is going to be used.
If you're going to use async/await you should be consistent. Mixing async with then doesn't make a lot of sense. This way you're either going to return some data, or throw an error.
async function getData(endpoint) {
try {
const response = await fetch(endpoint);
if (response.ok) {
const { data } = await response.json();
return data;
} else {
throw new Error('Response error.');
return undefined;
}
} catch (err) {
console.warn(err);
}
}
async function main() {
const endpoint = 'http://example.com';
const data = await getData(endpoint);
if (data && data.results) {
console.log(data.results.name);
} else {
console.warn('No results');
}
}
main();

Promise wont return valid value

I have this test I made just to check an API, but then i tryied to add an URL from a second fetch using as parameter a value obtained in the first fetch and then return a value to add in the first fecth. The idea is to add the image URL to the link. thanks in advance.
function script() {
const url = 'https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20'
const result = fetch(url)
.then( (res)=>{
if(res.ok) {
return res.json()
} else {
console.log("Error!!")
}
}).then( data => {
console.log(data)
const main = document.getElementById('main');
main.innerHTML=`<p><a href='${data.next}'>Next</a></p>`;
for(let i=0; i<data.results.length;i++){
main.innerHTML=main.innerHTML+`<p><a href=${getImageURL(data.results[i].url)}>${data.results[i].name}</a></p>`;
}
})
}
async function getImageURL(imgUrl) {
const resultImg = await fetch(imgUrl)
.then( (res)=> {
return res.json()
})
.then (data => {
console.log(data.sprites.other.dream_world.front_default);
})
return resultImg.sprites.other.dream_world.front_default;
}
In general, don't mix .then/.catch handlers with async/await. There's usually no need, and it can trip you up like this.
The problem is that your fulfillment handler (the .then callback) doesn't return anything, so the promise it creates is fulfilled with undefined.
You could return data, but really just don't use .then/.catch at all:
async function getImageURL(imgUrl) {
const res = await fetch(imgUrl);
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
const resultImg = await res.json();
return resultImg.sprites.other.dream_world.front_default;
}
[Note I added a check of res.ok. This is (IMHO) a footgun in the fetch API, it doesn't reject its promise on HTTP errors (like 404 or 500), only on network errors. You have to check explicitly for HTTP errors. (I wrote it up on my anemic old blog here.)]
There's also a problem where you use getImageURL:
// Incorrent
for (let i = 0; i < data.results.length; i++) {
main.innerHTML=main.innerHTML+`<p><a href=${getImageURL(data.results[i].url)}>${data.results[i].name}</a></p>`;
}
The problen here is that getImageURL, like all async functions, returns a promise. You're trying to use it as those it returned the fulfillment value you're expecting, but it can't — it doesn't have that value yet.
Instead, you need to wait for the promise(s) youre creating in that loop to be fulfilled. Since that loop is in synchronous code (not an async function), we'd go back to .then/.catch, and since we want to wait for a group of things to finish that can be done in parallel, we'd do that with Promise.all:
// ...
const main = document.getElementById('main');
const html = `<p><a href='${data.next}'>Next</a></p>`;
Promise.all(data.results.map(async ({url, name}) => {
const realUrl = await getImageURL(url);
return `<p><a href=${realUrl}>${name}</a></p>`;
}))
.then(paragraphs => {
html += paragraphs.join("");
main.innerHTML = html;
})
.catch(error => {
// ...handle/report error...
});
For one, your
.then (data => {
console.log(//...
at the end of the promise chain returns undefined. Just remove it, and if you want to console.log it, do console.log(resultImg) in the next statement/next line, after await.
This the final version that accomplish my goal. Just want to leave this just in case someone finds it usefull. Thanks for those who answer!
function script() {
const url = 'https://pokeapi.co/api/v2/pokemon/?offset=20&limit=20'
const result = fetch(url)
.then( (res)=>{
if(res.ok) {
return res.json()
} else {
console.log("Error!!")
}
}).then( data => {
console.log(data)
const main = document.getElementById('main');
main.innerHTML=`<p><a href='${data.next}'>Proxima Página</a></p>`;
Promise.all(data.results.map(async ({url, name}) => {
const realUrl = await getImageURL(url);
return `<div><a href=${realUrl}>${name}</a></div>`;
}))
.then(paragraphs => {
main.innerHTML=main.innerHTML+paragraphs;
})
.catch(error => {
console.log(error);
});
})
}
async function getImageURL(imgUrl) {
const res = await fetch(imgUrl);
if(!res.ok) {
throw new Error(`HTTP Error ${res.status}`)
}
const resultImg = await res.json();
return resultImg.sprites.other.dream_world.front_default
}

How do I setup this JS code to do better testing?

Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.
const $ = require('jquery')
const axios = require('axios')
let workerComplete = () => {
window.location.reload()
}
async function checkWorkerStatus() {
const worker_id = $(".worker-waiter").data('worker-id')
const response = await axios.get(`/v1/workers/${worker_id}`)
return response.data
}
function waitForWorker() {
if (!$('.worker-waiter').length) {
return
}
checkWorkerStatus().then(data => {
// delay next action by 1 second e.g. calling api again
return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}).then(worker_response => {
const working_statuses = ['queued', 'working']
if (worker_response && working_statuses.includes(worker_response.status)) {
waitForWorker()
} else {
workerComplete()
}
})
}
export {
waitForWorker,
checkWorkerStatus,
workerComplete
}
if (process.env.NODE_ENV !== 'test') $(waitForWorker)
Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'
import axios from 'axios'
import * as Worker from 'worker_waiter'
jest.mock('axios')
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() }
})
});
beforeEach(() => jest.resetAllMocks() )
afterEach(() => {
jest.restoreAllMocks();
});
describe('worker is complete after 2 API calls a', () => {
const worker_id = Math.random().toString(36).slice(-5) // random string
beforeEach(() => {
axios.get
.mockResolvedValueOnce({ data: { status: 'working' } })
.mockResolvedValueOnce({ data: { status: 'complete' } })
jest.spyOn(Worker, 'waitForWorker')
jest.spyOn(Worker, 'checkWorkerStatus')
document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
})
it('polls the correct endpoint twice a', async() => {
const endpoint = `/v1/workers/${worker_id}`
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint]])
expect(data).toMatchObject({"status": "working"})
})
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
expect(data).toMatchObject({"status": "complete"})
})
})
it('polls the correct endpoint twice b', async() => {
jest.mock('waitForWorker', () => {
expect(Worker.checkWorkerStatus).toBeCalled()
})
expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
await Worker.waitForWorker()
})
I think there are a couple things you can do here.
Inject status handlers
You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.
function waitForWorker(onComplete, onBusy) {
// instead of calling waitForWorker call onBusy.
// instead of calling workerComplete call onComplete.
}
Now to test, you really just need to create mock functions.
const onComplete = jest.fn();
const onBusy = jest.fn();
And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.
Return a promise
You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.
async function waitForWorker() {
let result = { status: 'empty' };
if (!$('.worker-waiter').length) {
return result;
}
try {
const working_statuses = ['queued', 'working'];
const data = await checkWorkerStatus();
if (data && working_statuses.includes(data.status)) {
await waitForWorker();
} else {
result = { status: 'complete' };
}
} catch (e) {
result = { status: 'error' };
}
return result;
}
The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.
If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.
async function waitForWorker(request) {
// ...
try {
const working_statuses = ['queued', 'working'];
const data = await request();
} catch (e) {
// ...
}
// ...
}
You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.
const result = await waitForWorker(checkWorkerStatus);
if (result.status === 'complete') {
workerComplete();
}

How to make async/await wait for an Observable to return

Fairly new to Angular, and struggling with Promises, Observables and async/await.
I have a function which needs the current user details to do some tasks.
For this, I wrote a fetch user detail method which returns a promise and the tasks are handled within the 'then' part.
The fetch method itself calls another method which invokes the http.get to get the user details from database, and returns an Observable.
I used async/await here for the function to wait for http.get to return.
However, this doesn't seem to work. I have a feeling this has something to do with how Observables/subscribe works, but I am unable to solve it.
Code snippet:
initPage() {
fetchCurrentUserDetails().then((user) => { //tasks dependent on current user
//task 1
//task 2
});
}
fetchCurrentUserDetails(): Promise<any> {
return Promise.resolve((async () => {
let currentUser = this.global.getUser();// check if user is defined already
let userId: string = sessionStorage.getItem('userid');
if (currentUser == undefined) {
let initProfile = new Promise(resolve => resolve(this.fetchDetailsFromDB(userId)));
const profile: any = await initProfile; //Waits, but returns before the Observable comes back
let user = new User();
// initialize user with the fetched values
user.id = profile.id; // Undefined, since value not returned yet
user.name = profile.user_name; // Undefined, since value not returned yet
// Set this user in a global variable
this.global.setUser(user);
}
return this.global.getUser();
})());
}
fetchDetailsFromDB(userId: string) {
//callProfileService has nothing but the http.get statement
this.callProfileService(userId).subscribe((response) => {
let profile = response.body.response.data.user;
return profile;
});
}
Edit: adding how I tried with toPromise:
fetchDetailsFromDB(userId: string) {
this.callUserProfileService(userId).toPromise().then((response) => {
let profile = response.body.response.data.user;
return profile;
});
Is this the right way to do this? If so, how to make the await wait for the Observable to return?
Indeed, you need the toPromise() method, but don't forget to return that promise (the return in the callback is not enough -- the function fetchDetailsFromDB needs to return a promise).
On the rest of your code: it is an antipattern to use Promise.resolve and new Promise like that: as a rule of thumb, don't create a new promise with either of these when you already have a promise to work with (e.g. from an API function call).
So here is how you could do it with async methods:
async fetchCurrentUserDetails(): Promise<any> {
//^^^^
let currentUser = this.global.getUser();
if (currentUser == undefined) {
const userId: string = sessionStorage.getItem('userid');
const profile: any = await this.fetchDetailsFromDB(userId);
currentUser = new User();
currentUser.id = profile.id;
currentUser.name = profile.user_name;
this.global.setUser(currentUser);
}
return currentUser;
}
async fetchDetailsFromDB(userId: string): Promise<any> {
const response = await this.callUserProfileService(userId).toPromise();
return response.body.response.data.user;
};
you can use callback
initPage() {
fetchCurrentUserDetails((user) => {
//task 1
//task 2
});
}
fetchCurrentUserDetails(callback) {
const currentUser = this.global.getUser();
if (!currentUser) {
const userId: string = sessionStorage.getItem('userid');
return this.callProfileService(userId).subscribe((response) => {
const profile = response.body.response.data.user;
let user = new User();
user.id = profile.id;
user.name = profile.user_name;
this.global.setUser(user);
return callback(user);
});
} else {
return callback(currentUser);
}
}

How to retrieve all posts of a user via Facebook Graph API using promises and recursion?

I am currently developing a web app which uses the Facebook Graph API.
What I would like to achieve is to get all posts of a user.
However, this is not that easy since I have to paginate the results.
At the moment I am struggeling with promises.
What I try to achieve is to fill an array with the post objects.
Therefore I use promises and recursion which does not work as expected.
My code currently looks as follows:
// Here I retrieve the user with his or her posts,
// just the first 25 due to pagination
if (accessToken) {
return new Promise(resolve => {
FB.api('/me?fields=id,name,posts&access_token=' + accessToken, response => {
this.get('currentUser').set('content', response);
resolve()
})
})
}
// Returns all posts of a given user
function getAllPostsOfUser(posts, postsArr) {
// Process each post of the current pagination level
for (var post of posts.data) {
// Only store meaningful posts
if (post !== undefined && post.message !== undefined) {
postsArr.push(post)
}
}
// Further posts are retrievalable via paging.next which is an url
if (posts.data.length !== 0 && posts.paging.next !== undefined) {
FB.api(posts.paging.next, response => {
getAllPostsOfUser(response, postsArr)
resolve()
})
}
return postsArr
}
var posts = getAllPostsOfUser(this.get('currentUser').content.posts, [])
// I want to use all the posts here
console.log(posts)
The problem I have is that I want to use the posts where the console.log is placed but when I log the posts array a lot of posts are missing.
I am sure that I did something wrong with the promises but I do not know what.
I would be glad if anyone could guide me to a solution.
Thank you in advance.
Try this:
function getAllPosts() {
return new Promise((resolve, reject) => {
let postsArr = [];
function recursiveAPICall(apiURL) {
FB.api(apiURL, (response) => {
if (response && response.data) {
//add response to posts array (merge arrays), check if there is more data via paging
postsArr = postsArr.concat(response.data);
if (response.paging && response.paging.next) {
recursiveAPICall(response.paging.next);
} else {
resolve(postsArr);
}
} else {
reject();
}
});
}
recursiveAPICall("/me/posts?fields=message&limit=100");
});
}
getAllPosts()
.then((response) => {
console.log(response);
})
.catch((e) => {
console.log(e);
});
Not tested, just a quick example I came up with. It returns a promise and uses a recursive function to get all entries. BTW, you don't need to add the Access Token. If you are logged in, the SDK will use it internally.
This is an old question that is already answered but I thought it could use a more modern answer, considering how many lines of code could be saved. This code has not been tested with the real API but it should work.
This function returns a promise of an array of posts.
async function getPosts(url = "/me/posts?fields=message&limit=100") {
const { error, paging, data } = (await new Promise(r => FB.api(url, r))) || {}
if (error || !data) throw new Error(error || "Could not get posts")
return data.concat(paging?.next ? await getPosts(paging.next) : [])
}
With comments:
async function getPosts(url = "/me/posts?fields=message&limit=100") {
// get response data out of callback
const { error, paging, data } = (await new Promise(r => FB.api(url, r))) || {}
// if there was an error or there wasn't any data, throw
if (error || !data) throw new Error(error || "Could not get posts")
// return this page's data + if there's a next page, recursively get its data
return data.concat(paging?.next ? await getPosts(paging.next) : [])
}
The function can then be consumed like so:
async function main() {
try {
const posts = await getPosts(); // the array of posts
console.log(posts);
} catch (error) {
console.error(error);
}
}
main();
Below is a snippet demonstrating the function using a fake API.
// fake api for testing purposes
const FB = {
api(url, callback) {
const pages = [
{
data: ["post1", "post2", "post3"],
paging: { next: 1 },
},
{
data: ["post4", "post5", "post6"],
paging: { next: 2 },
},
{
data: ["post7", "post8", "post9"],
},
];
if (typeof url !== "number") return callback(pages[0]);
return callback(pages[url]);
},
};
async function getPosts(url = "/me/posts?fields=message&limit=100") {
const { error, paging, data } = (await new Promise(r => FB.api(url, r))) || {}
if (error || !data) throw new Error(error || "Could not get posts")
return data.concat(paging?.next ? await getPosts(paging.next) : [])
}
async function main() {
try {
const posts = await getPosts(); // the array of posts
console.log(posts);
} catch (error) {
console.error(error);
}
}
main();
.as-console-wrapper{max-height:none !important;top: 0;}
Also see erikhagreis's ESM wrapper on GitHub.

Categories