im trying to load multiple images using an async function and a regular for function, and for some reason only the last image of the called is being loaded onto the dom..
I think it has to do with the global variable however I cant quite get a grip on why.
let imgEl;
const imageContainer = document.querySelector('.images');
function createImage(imgPath) {
return new Promise((resolve, reject) => {
imgEl = document.createElement('img');
imgEl.src = imgPath;
imgEl.addEventListener('load', () => {
document.querySelector('.images').append(imgEl);
resolve(imgEl);
});
imgEl.addEventListener('error', e => reject(e));
});
}
async function loadAll(...imgPaths) {
try {
const res = await Promise.all(
imgPaths.map(async imgPath => await createImage(imgPath))
);
res.forEach(el => el.classList.add('paralell'));
} catch (error) {
console.log(error);
}
}
loadAll('/img/img-1.jpg', '/img/img-2.jpg', '/img/img-3.jpg');
By the time the load event on any of the images has fired, the value of imgEl been overwritten by the last call to imgEl = document.createElement('img');.
This means the last image gets appended multiple times (initially adding it to the DOM and then moving it from where it is to … well … the same place several times).
The earlier images get garbage collected as there are no references left to them.
Define imgEl inside the function you pass to new Promise; don't make it a global. It doesn't need to be shared between multiple functions or multiple invocations of the same function.
Related
Hi so I asked this before but couldn't get an answer so was hoping this time someone would be kind enough to help out. I basically have a dropzone where i drop mulitple images then when i click on Submit i have a handlesubmit function. That function loops throught all the images and converts them to baseURL format using the FileReader() API. The problem is that once its all mapped and pushed into array i want to access that array and do stuff with it but right now the code isn't awaiting the .map execution and jumping straight to a console.log afterwards thats logging the array which is empty obviously. below is what i tried to do using promises but still couldn't get it to work:
const functionWithPromise = async file => {
let image = '';
let reader = new FileReader();
reader.readAsDataURL(file.file);
reader.onload = await function (){
image = reader.result;
console.log(image);
setArr([...arr,image]);
};//a function that returns a promise
reader.onloadend = function(){
Promise.resolve('ok');
}
};
const anAsyncFunction = async file => {
return functionWithPromise(file)
};
const getData = async (files) => {
return Promise.all(files.map(file => anAsyncFunction(file)))
};
//Clicking submit after all files are dropped triggers this.
const handleSubmit = (files, allFiles) => {
getData(files).then(r => console.log(arr))
.catch(err=>console.log(arr))
//I want to do some stuff here with the arr AFTER the mapping and pushing base64 values is complete.
};
I was following along a tutorial about 15 JS projects, and I pause and try to solve the project on my own before watching but I am a little stuck, I wanted to do it a bit different to get better with promises and to see how it is to use data outside of hardcoded one.
So I created a local JSON file and tried pulling the data but when the page is loaded the first iteration already is loaded and since the 'click' event didn't happen the data should not be displayed? (This confuses me a lot). Also the click event doesn't work and my currentItem isn't incrementing so it won't change when the click event happens so the next iteration won't change
let currentItem = 1;
// Event Listener for next button
nextBtn.addEventListener(
"click",
getData()
.then(function nextBtn(data) {
Reviews(data);
currentItem++;
})
.catch((err) => console.log(err))
);
// Get data from local or api
async function getData() {
const response = await fetch("reviews.json");
const data = await response.json();
return data;
}
// Display data in the UI
function Reviews(review) {
review.forEach(function (data) {
if (currentItem === data.id) {
personImg.src = data.img;
personaName.innerHTML = data.name;
personJob.innerHTML = data.job;
personInfo.innerHTML = data.text;
}
});
}
Take a look at the second parameter you're passing into addEventListener.
This parameter is supposed to be a function. This function will get executed when the click occurs.
However you're not passing it a function, you're actually calling the function getData().
I suspect what you really want is
nextBtn.addEventListener(
"click",
() => getData()
.then(function nextBtn(data) {
Reviews(data);
currentItem++;
})
.catch((err) => console.log(err))
);
Note that all I did was put () => before getData(), which makes it a function that doesn't get executed until the click actually occurs.
I want to:
upload an image into the browser (I do this with React Dropzone. My uploadImage function below runs onDrop of file into Dropzone.)
read the image as an actual image using the FileReader API.
save the image file as well as image metadata into state, using React Hooks.
make an API call that pushes this image to a server.
I am trying to confirm that I have the image at step 4, but I cannot confirm that via my console messages. I'm somehow stuck with the original state values despite log messages showing the state has already been updated.
I've tried adding extra asyncs to everything, and I've wrapped stuff in promises. I can confirm the readImage function is fine. I can confirm the run sequence is readImage, saveImage, setGraphic within saveImage, and then the console.log in uploadImage which should have the updated values but doesn't.
const [graphic, setGraphic] = useState({ ...nullGraphic });
const readImage = async (img) => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onerror = () => reader.abort();
reader.onabort = () => reject(new DOMException('Problem parsing input file.'));
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(img);
});
};
const saveImage = async (img) => {
const imageRead = await readImage(img);
console.log(imageRead);
await setGraphic({
...graphic,
image: imageRead,
imageName: img.name,
imageType: img.type,
});
};
const uploadImage = async (img) => {
await saveImage(img);
console.log(graphic);
});
render() {
console.log(graphic);
return( <some dom> )
}
this is my strange console sequence:
Form.js:112 starting saveImage
Form.js:95 (snipped value of imageRead)
Form.js:237 {snipped updated values of graphic}
Form.js:114 done with saveImage
Form.js:116 {snipped original values of graphic}
My Theory
The useState hook is doing something funky.
The process copies the value when it starts instead of reading it whenever the console.log is being read, so the uploadImage function gets the original values of the useState hook for the lifetime of that process.
It might become clear why this would not work if we would simplify all those React magic into :
function withState(initial, fn) {
function setState(updated) {
setTimeout(() => fn(updated, setState), 0);
//...
}
setState(initial);
}
Now we can illustrate what happens:
withState(0, (counter, setCount) => {
console.log("before", counter);
if(counter < 10) setCount(counter + 1);
console.log("after", counter);
});
As you can see, setCount will not change the state (count) directly. It will actually not change the local count variable at all. Therefore "before" and "after" will always log the same (as long as you don't mutate counter directly).
Instead calling setCount will be deffered (and batched in React), and then it will recall the whole component function passing in the new count.
Therefore it makes no sense trying to await the setState call, also it makes no sense to log the state before and after it (as it won't change inbetween). Log it once in a component, if you set the state the whole component executes again and the logging will log the new state.
I have a file called db.js from which I make all my firebase calls.
I am calling a function in db.js from another file called home.js
How do I make it that the firebase connection stays open and the data gets passed back to home.js? I can't use a promise because that closes the connection.
Here is the function from db.js:
export function getShopNames() {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").on('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
return stores
})
}
and I call it from home like this:
let stores = db.getShopNames()
I want it to work so if a new store gets added to the real-time database, the variable updates
There is no concept of file based scope in JavaScript. The listener will stay active from the moment you call on('value', until you either call off on that same location or until you load a new page.
But your return stores doesn't do anything meaningful right now. It returns a value from the callback function that nobody will ever see/use.
Data is loaded from Firebase asynchronously, which means you can't return it from a function in the normal way. By the time the return statement runs, the data hasn't loaded yet. That's why you'll usually return a so-called promise, which then resolves when the data has loaded.
In your function that'd be:
export function getShopNames() {
return new Promise(function(resolve, reject) {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").once('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
resolve(stores);
}, (error) => {
reject(error);
})
}
Now you can call this function like this:
getShopNames().then((shopnames) => {
console.log(shopnames);
})
Update: you commented that you also want to handle updates to the shop names, you can't use once() and can't use promises (since those only resolve once).
Instead pass in a custom callback, and invoke that every time the shop names change:
export function getShopNames(callback) {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").once('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
callback(stores);
})
}
And then call it like:
getShopnames(function(shopnames) {
console.log(shopnames);
});
I've been trying to figure out how Promises work with a rather simple example: one that fetches a number of images, loads it onto the page in order, counts the number of images loaded.
const addImg = url => {
fetch(url)
.then(validateResponse)
.then(readResponseAsBlob)
.then(showImage)
.catch(Error);
}
function showImage(responseAsBlob) {
const container = document.getElementById('img-container');
const imgElem = document.createElement('img');
container.appendChild(imgElem);
const imgUrl = URL.createObjectURL(responseAsBlob);
imgElem.src = imgUrl;
return imgUrl;
}
document.getElementById("add").onclick = () => {
document.getElementById("status").innerHTML = "Fetching...";
Promise.all(urls.map(url => addImg(url)))
.then(setTimeout(() => {
document.getElementById("status").innerHTML = document.getElementsByTagName("img").length + " images";
}, 0));
}
The addImg function fetches an image from the url, processes it as a blob and showImage renders adds a new img. When I try to add images from an array of urls, I have noticed a few problems I want to fix:
The images don't necessarily show up in order
the img count is not accurate
My first thought: if I deconstruct the addImg function so that it execute each step as a separate promise( fetch all -> then validate all -> then ... so on), it might work the way I intend it to, but I'm not sure if that's the right approach to it.
It might make more sense to you if you rewrote your code using async/await. If you rewrote your AJAX call as
const addImg = url => fetch(url)
.then(validateResponse)
.then(readResponseAsBlob)
.then(showImage)
.catch(Error);
And then you could do something like:
async function loadImages(){
for(image in imageList){
await addImg(image.url);
}
console.log('Images loaded');
}
This way your code will wait for each image load to complete before the next. Note that this isn't very performant but if you want them loading specifically in order then this is one you could achieve that easily.