0

I've edited the original post as it had two separate problems and ultimately AKX in the comments helped me solve what the real problem was.

I'm trying to do an image preview for multiple images using appendChild as shown in the MDN docs, but when I go to submit the form and want to reset the page I'm noticing that using appendChild can be kind of an anti-pattern in React. I want to come up with a solution where I store an array of image tags that I could then just set to empty on submit to clear the page, as opposed to having to query select them all with document and remove.

With the code below I'm only rendering the first image out of a batch upload of multiple files. Single image uploads one after another work fine.

  const previewMainImages = (e) => {
    const files = Object.values(e.currentTarget.files)
    if (mainImageFiles.length + 1 > 10) {
      setErr(errMessage = 'Only 10 images can be uploaded here')
      return
    }
    
    function readAndPreview(file) {
      var reader = new FileReader();
      reader.onloadend = () => {
        var image = <img src={reader.result} alt={file.name}/>
        setMain(mainImageFiles = [...mainImageFiles, file])
        setMainImages(mainImages.concat(image))
      }
      reader.readAsDataURL(file);
    }

    if (files) {
      files.forEach((f, i) => {
        console.log(f)
        readAndPreview(f)
      });
    }
  }
7
  • You don't return anything from the .map(), which is what React cares about. Whatever you do in reader.onloadend is out of React's "reach", as it were. You will need to do the asynchronous stuff (reading the files) separately and stuff the results into state. Commented Mar 25, 2021 at 17:42
  • @AKX Okay I understand that approach, I just couldn't get it working a little while ago. If you have a minute would you mind just glancing at the Update I just appended to the post? With that approach I could get one file to preview out of a batch but not the others. I could also add one image at a time and have them all preview, but not a batch. I know it's an asynchronous issue I'm just struggling to see where. Commented Mar 25, 2021 at 17:46
  • 1
    Yeah, you're going to have a bad time using the regular (non-callback) form of state setters with asynchronous code... Unfortunately I don't have the time to help you fix that right now, but you might want to consider storing an array or map of objects of the shape {file: ..., preview: ...} in state. Commented Mar 25, 2021 at 17:58
  • Also, it's not a good idea in general to store JSX elements in state. Just store the data you need to render those elements. Commented Mar 25, 2021 at 18:00
  • Ahh okay. Thanks I think I have an idea of what to try to next. Thanks for the JSX tip as well Commented Mar 25, 2021 at 18:02

1 Answer 1

1

I'm still learning javascript and React so I forgot about useRef. Also based on AKX's advice I also changed from storing image html tags to storing image data objects that I then map over to display the image tags in the form.

There seemed to be a problem where I was using two setState's and this was preventing the .forEach loop from iterating over all imgObjs.

So at the top of my file I define my useRef:

let mainImages = useRef([]);

I tried to remove both setStates within reader.onloadend but I found that the second setState for storing image files was actually needed to cause a re-render to ultimately display the images.

const previewMainImages = (e) => {
    const files = Object.values(e.currentTarget.files)
    if (mainImageFiles.length + 1 > 10) {
      setErr(errMessage = 'Only 10 images can be uploaded here')
      return
    }
    
    const readAndPreview = (file) => {
      var reader = new FileReader();
      reader.onloadend = () => {
        var imgObj = {};
        imgObj.src = reader.result
        imgObj.alt = file.name
        mainImages.current.push(imgObj)
        setMain(mainImageFiles = [...mainImageFiles, file])
      }
      reader.readAsDataURL(file);
    }

    if (files) {
      files.forEach((f, i) => {
        readAndPreview(f)
      });
    } 
  }

And then in the relevant part of the html I'm simply doing this:

        <div
          className={'mainPreview'}
        >
          <h2>Main Images</h2>
          <p>{errMessage}</p>
          <input
            type='file'
            multiple
            name='image'
            accept='.png, .jpg, jpeg'
            onChange={e => previewMainImages(e)}
          />
          {mainImages.current.map((img, i) => {
            return <img key={i} src={img.src} alt={img.alt} />
          })}
        </div>

And then my reset inputs work like this:

  const resetInputs = () => {
    setMain(mainImageFiles = []);
    setBody(bodyImageFiles = []);
    mainImages.current = [];
    setDescription(description = '');
    setTag(tag = '');
    setTags(tags = []);
    setErr(errMessage = '');
  }

I'm still having a problem with the file input values not being reset but I'm going to leave that for another day.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.