I am building a page to list products. So I’ve one input:file button to select multiple images and then I’m calling an API to upload that images on the server and displaying progress in UI with images. Here’s my code.
import axios from 'axios' import React, { useState } from 'react' import { nanoid } from 'nanoid' const Demo = () => { const [images, setImages] = useState([]) const handleImages = async (e) => { let newArr = [...images] for (let i = 0; i < e.target.files.length; i++) { newArr = [ ...newArr, { src: URL.createObjectURL(e.target.files[i]), img: e.target.files[i], uploaded: 0, completed: false, _id: nanoid(5), }, ] } setImages(newArr) await uploadImages(newArr) } const uploadProgress = (progress, arr, idx) => { let { total, loaded } = progress let inPercentage = Math.ceil((loaded * 100) / total) let imgs = [...arr] imgs[idx]['uploaded'] = inPercentage setImages([...imgs]) } const uploadImages = async (imageArr) => { for (let i = 0; i < imageArr.length; i++) { let formData = new FormData() formData.append('img', imageArr[i].img) let result = await axios.post( `http://localhost:3001/api/demo`, formData, { onUploadProgress: (progress) => uploadProgress(progress, imageArr, i), } ) if (result?.data) { let imgs = [...imageArr] imgs[i]['completed'] = true setImages([...imgs]) } } } return ( <div> <input type="file" multiple accept="image/*" onChange={handleImages} /> <div> <div className="img-container" style={{ display: 'flex' }}> {images.length ? ( <> {images.map((img) => ( <div style={{ position: 'relative' }}> <img style={{ width: '200px', height: 'auto', marginRight: '10px', }} src={img.src} alt="alt" /> {!img.completed && ( <div style={{ background: 'rgba(0,0,0,0.3)', padding: '4px', position: 'absolute', top: '10px', left: '10px', color: 'white', borderRadius: '5px', }} > {img.uploaded}% </div> )} </div> ))} </> ) : null} </div> </div> </div> ) } export default Demo
As the useState is asynchronous, I can’t directly pass that react state to my API handler. So now the issue is, suppose I’m selecting 3 images to upload, and before the execution of my “uploadImages” function completes, I try to select other images to upload, It’s not working as expected and I know the reason why it’s not working the way intend to. But don’t know the solution.
Issue: suppose, the user first tries to upload 3 images. the first instance of “uploadImages” will begin its execution with parameter newArr which would have 3 images and we’re setting it to react-state “images”. But now when the user tries to upload other images before letting the first ones finish, another instance of “uploadImages” will begin its execution and now in newArr parameter, it would have an array of newly added images and this method will try to set state “images”.
Now I don’t know how can I handle this. Preview
Advertisement
Answer
There are two problems.
- Each time
uploadProgress
is run, it’s using the images array that was passed to it by theuploadImages
function. In other words, if you start uploading image A, you trigger an instance ofuploadProgress
running withimageArr = [A]
. If you add image B, you trigger another separate instance of uploadProgress running withimageArr = [A,B]
. Since you’re setting state using those separateimageArr
s inuploadProgress
, theimages
state swaps from being the first array to the second array and back. (You can see this if you log from insideuploadProgress
.)
const uploadProgress = (progress, arr, idx) => { let { total, loaded } = progress let inPercentage = Math.ceil((loaded * 100) / total) let imgs = [...arr] console.log(imgs.length) // will toggle between 1 and 2 imgs[idx]['uploaded'] = inPercentage setImages([...imgs]) }
As others have said, you can solve this by using the functional setState pattern.
const uploadProgress = (progress, idx) => { let { total, loaded } = progress let inPercentage = Math.ceil((loaded * 100) / total) setImages(arr => { let imgs = [...arr] imgs[idx]['uploaded'] = inPercentage return imgs }) }
- Second, each time you trigger
uploadImages
it starts the upload for every image, cumulatively. That means if you upload image A, wait a second, then add image B, it’ll start uploading image A again and you’ll end up with two different upload of image A. If you then were to add image C, you’d get three copies of image A, two copies of image B, and one of image C. You can solve this by preventing upload of images that have progress value or add a new property that indicates the image upload process has been started already.
const uploadImages = async (imageArr) => { for (let i = 0; i < imageArr.length; i++) { if (imageArr[i].progress === 0) { // don't upload images that have already started