1

I am trying to implement a multiple image file selector in my React app.

This is my input element:

<input
type="file"
multiple
onChange={handleImageChange}
/>

{renderPhotos(venueImages)}

These are the functions that are called when files are chosen:

  const [venueImages, setVenueImages] = useState([]);`
  const renderPhotos = source => {
    console.log(source); ////////log 1

    return source.map(photo => {
      return <img src={photo} key={photo} />;
    });
  };

  const handleImageChange = e => {
    if (e.target.files) {
      const filesArray = Array.from(e.target.files);
      console.log(filesArray); ///////// log 2
      filesArray.forEach(file => {
        const tempUrl = URL.createObjectURL(file);
        console.log(tempUrl); ////// log 3

        setVenueImages([...venueImages, tempUrl]);
      });
    }
  };

I call renderPhotos to show a preview off all the selected photos before uploading.

The issue I'm facing is as follow: If I choose, for example, 5 photos, only 1 would end up being rendered on screen. I've inserted console logs in handleImageChange, and what I get logged is confusing me even more. The second log (I've numbered them in my code) prints an array of 5 files. After from log 3 that I'll get 5 logs of the newly generated temporary URLs for each of the files.

But log 1, would only get printed once.

Now - if I'll click the input element to choose more files, I'll end up with another rendered image. So basically everytime I choose images, no matter how many I've chosen, I'll only get one more image rendered.

Tsabary
  • 3,119
  • 2
  • 24
  • 66
  • 1
    why don't you call setVenueImages only once instead of once for each image? which image do you see, the first one loaded? the last one? – grokked Jan 09 '20 at 10:15
  • @grokked I see the last one. If want to use it once, how would I do it? Wouldn't I need to create a new array, then push to it every newly generated url, and then call my setVenueImages command? Wouldn't that be unnecessary code? I'll try it now. – Tsabary Jan 09 '20 at 10:19
  • Ok yup! That works :) You can add it as an answer and I'll approve it. But I still don't understand, why did pushing into an array, and then setting my venues with that array, is any different than pushing each item in its turn? – Tsabary Jan 09 '20 at 10:22

2 Answers2

2

The problem is that you are referencing the venueImages array in your setVenueImages call. Because the state update performed by setVenueImages is asynchronous, then you cannot be certain that venueImages contains all of the previous state updates.

The set state function can be passed a function that takes the old value, instead of just passing it the new value. This will ensure that your state updates are sequential. Replace your setVenueImages call with this:

setVenueImages(prevImages => [...prevImages, tempUrl]);

An additional change that I will suggest is to perform a concatenation of all images, instead of adding them one by one. This should be faster.

const handleImageChange = e => {
  if (e.target.files) {
    const filesArray = Array.from(e.target.files).map(file => URL.createObjectURL(file));
    console.log(filesArray); ///////// log 2
    setVenueImages(prevImages => prevImages.concat(filesArray));
  }
};
Billy Brown
  • 2,272
  • 23
  • 25
  • Thanks that works! I tried your second suggestion, but when I don't generated the temporary URL the previews don't show properly (it doesn't load an image just show the default image "error" placeholder). Am I missing something? Or was that method intended just to load the files into an array before upload? – Tsabary Jan 09 '20 at 10:28
  • 1
    @Tsabary ah yes, sorry: I missed the part where you created the URL. Updated my answer. – Billy Brown Jan 09 '20 at 10:39
  • 1
    I know this didn't originate from your, but I like your solution. But how do you avoid memory leaks with `createObjectURL` ? Don't you need to add `revokeObjectURL()` to remove the old image URL from memory somewhere? [reference](https://stackoverflow.com/questions/38049966/get-image-preview-before-uploading-in-react/57781164#comment94991328_54060913) – zipzit Mar 11 '20 at 02:42
  • @zipzit I think the difference is that here [`URL.createObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) is only called for the new files before they are added to the state, rather than being called for every item in the new state at every state update. As one and only one object URL is created per uploaded file, then there is no memory leak caused by this method. Of course it is important to revoke the object URL later if it is no longer needed. – Billy Brown Mar 11 '20 at 20:47
1

That is happening because when you are saving the tempUrl, only one url is getting saved. Also do not set the state by adding images one by one.

Updated version of your handleImageChange function can be

const handleImageChange = e => {
    if (e.target.files) {
      const filesArray = Array.from(e.target.files);
      const tempUrls = filesArray.map(file => URL.createObjectURL(file)))
      setVenueImages([...venueImages, ...tempUrls])
    }
};
shivamragnar
  • 383
  • 2
  • 10