I have a problem with prefetching resources for react app. I have a function which should fetch json resources and assign them to a object properties in a loop. The function should then return the object with jsons allready resolved, but instead of it returns the object with properties as unresolved promises.
Is it possible to stop executing js code until the promises are fullfiled so the function will return correct data ?
Yes, something like a synchronous request.
async function getJson (file, url) {
const response = await fetch(`${url}/${file}.json`);
const json = await response.json();
return json;
}
async function getTranslations(files, url) {
let r = {};
files.map( (file, i) => {
r[file] = await getJson(file, url);
});
return r;
}
I don't think you should be using 'map' like that - to populate an object you're better with forEach. Though I couldn't guarantee that's the issue.
async function getTranslations(files, url) {
let r = {};
files.forEach(file => r[file] = await getJson(file, url));
return r;
}
Related
I have multiple functions that pass data from one to another. I moved one of the functions to the backend and receive data as API with Axios. Now I cannot manage to assign data from Axios to some local variable.
Simple code would be like:
function function1()
{
axios({get, url})
.then(response => {
globalVariable = response.data;
function2(globalVariable);
}
function function2(globalVariable)
{
const local = globalVariable;
return local;
}
And then inside of function3, I want to do:
function function3()
{
const from_local = function2()
from_local
}
When I try this I receive undefined result. Please help.
This is what promises are for. No need for globals or jumping through hoops to get the data out. Just remember to await any function that's async, (like axios) and annotate any function that contains an "await" as being async.
// note "async" because it contains await
async function backend() {
// note await because axios is async
const response = await axios({get, url});
return response.data;
}
// same thing up the calling chain
async function middleend() {
const local = await backend();
return local;
}
async function frontend() {
const local = await middleend();
console.log('ta da! here\'s the data', local);
}
It looks like you are looking for some sort of piping asynchronous operation. By piping I mean result of one function execution will be feed to another.
So basically function1 which is mimicking a axios operation here.
// making an api call
function function1() {
return fetch('https://jsonplaceholder.typicode.com/todos/1').then((d) =>
d.json()
);
}
// some random function
function function2(data) {
console.log(' Calling function 2 ');
return data?.title;
}
// some random function
function function3(data) {
console.log(' Calling function 3 ');
return `Hello ${data}`;
}
/** a function to resolve functions sequentially. The result of first
function will be input to another function.
Here ...fns is creating an array like object
so array operations can be performed here **/
const runAsynFunctions = (...fns) => {
return (args) => {
return fns.reduce((acc, curr) => {
return acc.then(curr);
}, Promise.resolve(args));
};
};
// calling runAsynFunctions with and passing list of
// functions which need to resolved sequentially
const doOperation = runAsynFunctions(function2, function3);
// resolving the api call first and the passing the result to
// other functions
function1().then(async(response) => {
const res = await doOperation(response);
console.log(res);
});
I'd like to reuse the same code in a loop. This code contains promises. However, when iterating, this code results in an error.
I've tried using for and while loops. There seems to be no issue when I use the for loop for a single iteration.
Here is a minimal version of my code:
var search_url = /* Some initial URL */
var glued = "";
for(var i = 0; i < 2; i++)
{
const prom = request(search_url)
.then(function success(response /* An array from a XMLHTTPRequest*/) {
if (/* Some condition */)
{
search_url = /* Gets next URL */
glued += processQuery(response[0]);
} else {
console.log("Done.")
}
})
.catch(function failure(err) {
console.error(err.message); // TODO: do something w error
})
}
document.getElementById('api-content').textContent = glued;
I expect the results to append to the variable glued but instead, I get an error: failure Promise.catch (async) (anonymous) after the first iteration of the loop.
Answer:
You can use the Symbol.iterator in accordance with for await to perform asynchronous execution of your promises. This can be packaged up into a constructor, in the example case it's called Serial (because we're going through promises one by one, in order)
function Serial(promises = []) {
return {
promises,
resolved: [],
addPromise: function(fn) {
promises.push(fn);
},
resolve: async function(cb = i => i, err = (e) => console.log("trace: Serial.resolve " + e)) {
try {
for await (let p of this[Symbol.iterator]()) {}
return this.resolved.map(cb);
} catch (e) {
err(e);
}
},
[Symbol.iterator]: async function*() {
this.resolved = [];
for (let promise of this.promises) {
let p = await promise().catch(e => console.log("trace: Serial[Symbol.iterator] ::" + e));
this.resolved.push(p);
yield p;
}
}
}
}
What is the above?
It's a constructor called Serial.
It takes as an argument an array of Functions that return Promises.
The functions are stored in Serial.promises
It has an empty array stored in Serial.resolved - this will store the resolved promise requests.
It has two methods:
addPromise: Takes a Function that returns a Promise and adds it to Serial.promises
resolve: Asynchronously calls a custom Symbol.iterator. This iterator goes through every single promise, waits for it to be completed, and adds it to Serial.resolved. Once this is completed, it returns a map function that acts on the populated Serial.resolved array. This allows you to simply call resolve and then provide a callback of what to do with the array of responses. A.e. .resolve()((resolved_requests) => //do something with resolved_requests)
Why does it work?
Although many people don't realize this Symbol.iterator is much more powerful than standard for loops. This is for two big reasons.
The first reason, and the one that is applicable in this situation, is because it allows for asynchronous calls that can affect the state of the applied object.
The second reason is that it can be used to provide two different types of data from the same object. A.e. You may have an array that you would like to read the contents of:
let arr = [1,2,3,4];
You can use a for loop or forEach to get the data:
arr.forEach(v => console.log(v));
// 1, 2, 3, 4
But if you adjust the iterator:
arr[Symbol.iterator] = function* () {
yield* this.map(v => v+1);
};
You get this:
arr.forEach(v => console.log(v));
// 1, 2, 3, 4
for(let v of arr) console.log(v);
// 2, 3, 4, 5
This is useful for many different reasons, including timestamping requests/mapping references, etc. If you'd like to know more please take a look at the ECMAScript Documentation: For in and For Of Statements
Use:
It can be used by calling the constructor with an Array of functions that return Promises. You can also add Function Promises to the Object by using
new Serial([])
.addPromise(() => fetch(url))
It doesn't run the Function Promises until you use the .resolve method.
This means that you can add promises ad hoc if you'd like before you do anything with the asynchronous calls. A.e. These two are the same:
With addPromise:
let promises = new Serial([() => fetch(url), () => fetch(url2), () => fetch(url3)]);
promises.addPromise(() => fetch(url4));
promises.resolve().then((responses) => responses)
Without addPromise:
let promises = new Serial([() => fetch(url), () => fetch(url2), () => fetch(url3), () => fetch(url4)])
.resolve().then((responses) => responses)
Data:
Since I can't really replicate your data calls, I opted for JSONPlaceholder (a fake online rest api) to show the promise requests in action.
The data looks like this:
let searchURLs = ["https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/todos/2",
"https://jsonplaceholder.typicode.com/todos/3"]
//since our constructor takes functions that return promises, I map over the URLS:
.map(url => () => fetch(url));
To get the responses we can call the above data using our constructor:
let promises = new Serial(searchURLS)
.resolve()
.then((resolved_array) => console.log(resolved_array));
Our resolved_array gives us an array of XHR Response Objects. You can see that here:
function Serial(promises = []) {
return {
promises,
resolved: [],
addPromise: function(fn) {
promises.push(fn);
},
resolve: async function(cb = i => i, err = (e) => console.log("trace: Serial.resolve " + e)) {
try {
for await (let p of this[Symbol.iterator]()) {}
return this.resolved.map(cb);
} catch (e) {
err(e);
}
},
[Symbol.iterator]: async function*() {
this.resolved = [];
for (let promise of this.promises) {
let p = await promise().catch(e => console.log("trace: Serial[Symbol.iterator] ::" + e));
this.resolved.push(p);
yield p;
}
}
}
}
let searchURLs = ["https://jsonplaceholder.typicode.com/todos/1", "https://jsonplaceholder.typicode.com/todos/2", "https://jsonplaceholder.typicode.com/todos/3"].map(url => () => fetch(url));
let promises = new Serial(searchURLs).resolve().then((resolved_array) => console.log(resolved_array));
Getting Results to Screen:
I opted to use a closure function to simply add text to an output HTMLElement.
This is added like this:
HTML:
<output></output>
JS:
let output = ((selector) => (text) => document.querySelector(selector).textContent += text)("output");
Putting it together:
If we use the output snippet along with our Serial object the final functional code looks like this:
let promises = new Serial(searchURLs).resolve()
.then((resolved) => resolved.map(response =>
response.json()
.then(obj => output(obj.title))));
What's happening above is this:
we input all our functions that return promises. new Serial(searchURLS)
we tell it to resolve all the requests .resolve()
after it resolves all the requests, we tell it to take the requests and map the array .then(resolved => resolved.map
the responses we turn to objects by using .json method. This is necessary for JSON, but may not be necessary for you
after this is done, we use .then(obj => to tell it to do something with each computed response
we output the title to the screen using output(obj.title)
Result:
let output = ((selector) => (text) => document.querySelector(selector).textContent += text)("output");
function Serial(promises = []) {
return {
promises,
resolved: [],
addPromise: function(fn) {
promises.push(fn);
},
resolve: async function(cb = i => i, err = (e) => console.log("trace: Serial.resolve " + e)) {
try {
for await (let p of this[Symbol.iterator]()) {}
return this.resolved.map(cb);
} catch (e) {
err(e);
}
},
[Symbol.iterator]: async function*() {
this.resolved = [];
for (let promise of this.promises) {
let p = await promise().catch(e => console.log("trace: Serial[Symbol.iterator] ::" + e));
this.resolved.push(p);
yield p;
}
}
}
}
let searchURLs = ["https://jsonplaceholder.typicode.com/todos/1", "https://jsonplaceholder.typicode.com/todos/2", "https://jsonplaceholder.typicode.com/todos/3"].map(url => () => fetch(url));
let promises = new Serial(searchURLs).resolve()
.then((resolved) => resolved.map(response =>
response.json()
.then(obj => output(obj.title))));
<output></output>
Why go this route?
It's reusable, functional, and if you import the Serial Constructor you can keep your code slim and comprehensible. If this is a cornerstone of your code, it'll be easy to maintain and use.
Using it with your code:
I will add how to specifically use this with your code to fully answer your question and so that you may understand further.
NOTE glued will be populated with the requested data, but it's unnecessary. I left it in because you may have wanted it stored for a reason outside the scope of your question and I don't want to make assumptions.
//setup urls:
var search_urls = ["https://jsonplaceholder.typicode.com/todos/1", "https://jsonplaceholder.typicode.com/todos/2"];
var request = (url) => () => fetch(url);
let my_requests = new Serial(search_urls.map(request));
//setup glued (you don't really need to, but if for some reason you want the info stored...
var glued = "";
//setup helper function to grab title(this is necessary for my specific data)
var addTitle = (req) => req.json().then(obj => (glued += obj.title, document.getElementById('api-content').textContent = glued));
// put it all together:
my_requests.resolve().then(requests => requests.map(addTitle));
Using it with your code - Working Example:
function Serial(promises = []) {
return {
promises,
resolved: [],
addPromise: function(fn) {
promises.push(fn);
},
resolve: async function(cb = i => i, err = (e) => console.log("trace: Serial.resolve " + e)) {
try {
for await (let p of this[Symbol.iterator]()) {}
return this.resolved.map(cb);
} catch (e) {
err(e);
}
},
[Symbol.iterator]: async function*() {
this.resolved = [];
for (let promise of this.promises) {
let p = await promise().catch(e => console.log("trace: Serial[Symbol.iterator] ::" + e));
this.resolved.push(p);
yield p;
}
}
}
}
//setup urls:
var search_urls = ["https://jsonplaceholder.typicode.com/todos/1", "https://jsonplaceholder.typicode.com/todos/2"];
var request = (url) => () => fetch(url);
let my_requests = new Serial(search_urls.map(request));
//setup glued (you don't really need to, but if for some reason you want the info stored...
var glued = "";
//setup helper function to grab title(this is necessary for my specific data)
var addTitle = (req) => req.json().then(obj => (glued += obj.title, document.getElementById('api-content').textContent = glued));
// put it all together:
my_requests.resolve().then(requests => requests.map(addTitle));
<div id="api-content"></div>
Final Note
It's likely that we will be seeing a prototypal change to the Promise object in the future that allows for easy serialization of Promises. Currently (7/15/19) there is a TC39 Proposal that does add a lot of functionality to the Promise object but it hasn't been fully vetted yet, and as with many ideas trapped within the Proposal stage, it's almost impossible to tell when they will be implemented into Browsers, or even if the idea will stagnate and fall off the radar.
Until then workarounds like this are necessary and useful( the reason why I even went through the motions of constructing this Serializer object was for a transpiler I wrote in Node, but it's been very helpful beyond that! ) but do keep an eye out for any changes because you never know!
Hope this helps! Happy Coding!
Your best bet is probably going to be building up that glued variable with recursion.
Here's an example using recursion with a callback function:
var glued = "";
requestRecursively(/* Some initial URL string */, function() {
document.getElementById('api-content').textContent = glued;
});
function requestRecursively(url, cb) {
request(url).then(function (response) {
if (/* Some condition */) {
glued += processQuery(response[0]);
var next = /* Gets next URL string */;
if (next) {
// There's another URL. Make another request.
requestRecursively(next, cb);
} else {
// We're done. Invoke the callback;
cb();
}
} else {
console.log("Done.");
}
}).catch(function (err) {
console.error(err.message);
});
}
This is my axios Request to call the API.
export function axiosGet (url) {
return opsutils.get(url)
.then(function (response) {
return response.data.data;
})
.catch(function (error) {
return 'An error occured..' + error;
})
}
From here i'm calling it asynchrously
async getDirList(data){
this.domainDir=data.domain_name
var apiurl="some URL"
var stat = await axiosGet(apiurl)
if (status){
this.domainlog= stat
}
From here i'm calling the async func defined above
Notify(data){
var filelist = this.getDirList(data)
if(filelist){
var status = createNotification(data.domain_name,message_stripped,'Degrading web server performance')
}
The ideal should be like this that it should go forward only after the promise is resolved ,right now the var filelist got empty.
How do i get to solve this problem ?Thanks in advance
The problem is this.getDirList(data) is not being accessed asynchronously as well. Remember, because that is async now, it's returning a promise, so you either need to chain it with .then():
Notify(data){
var filelist = this.getDirList(data)
.then(data => {
var status = createNotification(data.domain_name,message_stripped,'Degrading web server performance')
});
}
Or turn Notify() into an async function as well:
async Notify(data){
var filelist = await this.getDirList(data);
if(filelist){
var status = createNotification(data.domain_name,message_stripped,'Degrading web server performance')
}
Additionally, make sure you're actually returning data from getDirList so you can utilize the return value when you await it.
I am having trouble wrapping my head around returning json data from a fetch() call in one function, and storing that result in a variable inside of another function. Here is where I am making the fetch() call to my API:
function checkUserHosting(hostEmail, callback) {
fetch('http://localhost:3001/activities/' + hostEmail)
.then((response) => {
response.json().then((data) => {
console.log(data);
return data;
}).catch((err) => {
console.log(err);
})
});
}
And this is how I am trying to get the returned result:
function getActivity() {
jsonData = activitiesActions.checkUserHosting(theEmail)
}
However, jsonData is always undefined here (which I am assuming is because the async fetch call has not finished yet when attempting to assign the returned value to jsonData. How do I wait long enough for the data to be returned from the fetch call so that I can properly store it inside of jsonData?
always return the promises too if you want it to work:
- checkUserHosting should return a promise
- in your case it return a promise which return the result data.
function checkUserHosting(hostEmail, callback) {
return fetch('http://localhost:3001/activities/' + hostEmail)
.then((response) => {
return response.json().then((data) => {
console.log(data);
return data;
}).catch((err) => {
console.log(err);
})
});
}
and capture it inside .then() in your main code:
function getActivity() {
let jsonData;
activitiesActions.checkUserHosting(theEmail).then((data) => {
jsonData = data;
}
}
EDIT:
Or even better, use the new syntax as #Senthil Balaji suggested:
const checkUserHosting = async (hostEmail, callback) => {
let hostEmailData = await fetch(`http://localhost:3001/activities/${hostEmail}`)
//use string literals
let hostEmailJson = await hostEmailData.json();
return hostEmailJson;
}
const getActivity = async () => {
let jsonData = await activitiesActions.checkUserHosting(theEmail);
//now you can directly use jsonData
}
You're partially right. It's because you're trying to get the result of this asynchronous call in a synchronous fashion. The only way to do this is the same way you deal with any other promise. Via a .then callback. So for your snippet:
function getActivity() {
return activitiesActions.checkUserHosting(theEmail).then((jsonData) => {
// Do things with jsonData
})
}
Any function that depends on an asynchronous operation must itself become asynchronous. So there's no escaping the use of .then for anything that requires the use of the checkUserHosting function.
You can make use of new ES6 and Es7 syntax and what others have written is also correct, but this can be more readable and clean,
you are trying to get aysnc value synchronously, here jsonData will be undefined because, you move to next line of execution before async function(checkUserHosting) is finish executing, this can be written as follows
const getActivity = async () => {
let jsonData = await activitiesActions.checkUserHosting(theEmail);
//now you can directly use jsonData
}
and you can write checkUserHosting in a different using new syntax like this
const checkUserHosting = async (hostEmail, callback) => {
let hostEmailData = await fetch(`http://localhost:3001/activities/${hostEmail}`)
//use string literals
let hostEmailJson = await hostEmailData.json();
return hostEmailJson;
}
What i want to do is to upload file on server, then get URL of uploaded file and preview it. Files can be more than one. For that purpose i have written following code:
let filesURL=[];
let promises=[];
if(this.state.files_to_upload.length>0) {
for(let i=0; i<this.state.files_to_upload.length; i++) {
promises.push(this.uploadFilesOnServer(this.state.files_to_upload[i]))
}
Promise.all(promises).then(function(result){
console.log(result);
result.map((file)=>{
filesURL.push(file);
});
});
console.log(filesURL);
}
const uploadedFilesURL=filesURL;
console.log(uploadedFilesURL);
console.log(filesURL); give me the values returned by Promise.all.
And i want to use these values only when Promise.all completes properly. But, i am facing problem that lines console.log(uploadedFilesURL); excutes first irrespective of Promise.all and give me undefined values.I think i am not using promises correctly, can anyone please help me?
uploadFileOnServer code is:
uploadFilesOnServer(file)
{
let files=[];
let file_id='';
const image=file;
getImageUrl().then((response) => {
const data = new FormData();
data.append('file-0', image);
const {upload_url} = JSON.parse(response);
console.log(upload_url);
updateProfileImage(upload_url, data).then ((response2) => {
const data2 = JSON.parse(response2);
file_id=data2;
console.log(file_id);
files.push(file_id);
console.log(files);
});
});
return files;
}
No, promise is asynchronous and as such, doesn't work the way you think. If you want to execute something after a promise completed, you must put it into the promise's then callback. Here is the example based on your code:
uploadFilesOnServer(file) {
let files=[];
let file_id='';
const promise = getImageUrl()
.then((imageUrlResponse) => {
const data = new FormData();
data.append('file-0', file);
const { upload_url } = JSON.parse(imageUrlResponse);
console.log(upload_url);
return updateProfileImage(upload_url, data);
})
.then ((updateImageResponse) => {
file_id= JSON.parse(updateImageResponse);
console.log(file_id);
files.push(file_id);
console.log(files);
return files;
});
return promise;
}
let filesPromise = Promise.resolve([]);
if(this.state.files_to_upload.length > 0) {
const promises = this.state.files_to_upload.map((file) => {
return this.uploadFilesOnServer(file);
});
filesPromise = Promise.all(promises).then((results) => {
console.log(results);
return [].concat(...results);
});
}
// This is the final console.log of you (console.log(uploadedFilesURL);)
filesPromise.then((filesUrl) => console.log(filesUrl));
A good book to read about ES6 in general and Promises in particular is this book Understanding ECMAScript 6 - Nicholas C. Zakas
Edit:
Here is an simple explanation of the example code:
The uploadFilesOnServer is a function that takes a file, upload it and will return the file URL when the upload completes in the future in the form of a promise. The promise will call its then callback when it gets the url.
By using the map function, we create a list of url promises, the results we've got from executing uploadFilesOnServer on each file in the list.
The Promise.all method waits for all the promises in the list to be completed, joins the list of url results and create a promise with the result which is the list of urls. We need this because there is no guarantee that all of the promises will complete at once, and we need to gather all the results in one callback for convenience.
We get the urls from the then callback.
You have to do this on the .then part of your Promise.all()
Promise.all(promises)
.then(function(result){
console.log(result);
result.map((file)=>{
filesURL.push(file);
});
return true; // return from here to go to the next promise down
})
.then(() => {
console.log(filesURL);
const uploadedFilesURL=filesURL;
console.log(uploadedFilesURL);
})
This is the way async code works. You cannot expect your console.log(filesURL); to work correctly if it is being called syncronously after async call to fetch files from server.
Regarding to your code there are several problems:
1.uploadFilesOnServer must return Promise as it is async. Therefore:
uploadFilesOnServer(file)
{
let files=[];
let file_id='';
const image=file;
return getImageUrl().then((response) => {
const data = new FormData();
data.append('file-0', image);
const {upload_url} = JSON.parse(response);
console.log(upload_url);
updateProfileImage(upload_url, data).then ((response2) => {
const data2 = JSON.parse(response2);
file_id=data2;
console.log(file_id);
files.push(file_id);
console.log(files);
return files;
});
});
}
2.Inside your main function body you can assess results of the Promise.all execution only in its respective then handler.
As a side note I would recomment you to use es7 async/await features with some transpilers like babel/typescript. This will greatly reduce the nesting/complications of writing such async code.