skip to Main Content

We want to upload image files and are able to get the uploaded images from the child component to the parent component through the parameter and a function to set the specific prop we are passing to the child component.
However, we can’t create a new array out of two arrays, one being the local state of the parent component, the other being the newly uploaded images sent from the child.

Parent component — ImageUpload.js

const EntryEditPage = () => {

    const [currentImages, setCurrentImages] = useState([]);

    const handleImagesUpload = (uploadedImages) => {
        console.log('handleImagesUpload -- curIm:', currentImages);  // output: right image(s)  
        console.log('handleImagesUpload -- uplIm:', uploadedImages); // output: right image(s)
        console.log('handleImagesUpload -- expected result:', [
          ...currentImages,
          ...uploadedImages,
        ]);                                                          // output: []

        setCurrentImages((prevState) => [...prevState, ...uploadedImages]);
  };

    return (
        <section>
            <ImageUpload
              currentImages={currentImages}
              handleImagesUpload={(images) => handleImagesUpload(images)}
            />
    
            <section>
              {currentImages.map((image) => (
                <div
                  key={image.name}
                >
                  <img src={image.url} alt={image.name} />
                  />
                </div>
              ))}
            </section>
         </section>
    
    )

}

export default EntryEditPage;

Child component:

const ImageUpload = ({ currentImages, handleImagesUpload }) => {
  const [uploadedImages, setUploadedImages] = useState([]);
  const [isErrorFilesLimit, setIsErrorFilesLimit] = useState(false);
  const [errorFileName, setErrorFileName] = useState('');
  const [errorFileType, setErrorFileType] = useState('');
  const [errorFileSize, setErrorFileSize] = useState('');

  const allowedFileExtensions = /.(jpe?g|png)$/i;
  const maxFiles = 10;
  const maxFileSize = 10485760; // 10MB per image

  const { register } = useFormContext();

  const addError = (errors, newError) =>
    `${errors}${errors ? 'n' : ''}${newError}`;

  const handleChange = (event) => {
    const validUploadedImages = [];
    let currentErrorsFileName = '';
    let currentErrorsFileType = '';
    let currentErrorsFileSize = '';

    [...event.target.files].every((file) => {
      // check FILES LIMIT
      if ([...currentImages, ...validUploadedImages].length >= maxFiles) {
        setIsErrorFilesLimit(true);
        return false;
      }

      // check FILE NAME
      if (
        [...currentImages, ...validUploadedImages].findIndex(
          (f) => f.name === file.name
        ) >= 0
      ) {
        currentErrorsFileName = addError(currentErrorsFileName, file.name);
        return true;
      }

      // check FILE TYPE
      if (!file.name.match(allowedFileExtensions)) {
        currentErrorsFileType = addError(currentErrorsFileType, file.name);
        return true;
      }

      // check FILE SIZE
      if (file.size > maxFileSize) {
        currentErrorsFileSize = addError(currentErrorsFileSize, file.name);
        return true;
      }

      // FILE checked and OK
      const reader = new FileReader();
      reader.readAsDataURL(file);

      reader.onload = () => {
        const constructedFile = {
          name: file.name,
          url: reader.result,
          file,
        };

        validUploadedImages.push(constructedFile);
      };
      return true;
    });

    !currentErrorsFileName || setErrorFileName(currentErrorsFileName);
    !currentErrorsFileType || setErrorFileType(currentErrorsFileType);
    !currentErrorsFileSize || setErrorFileSize(currentErrorsFileSize);

    if (validUploadedImages.length > 0) {
      setUploadedImages(validUploadedImages);
    }

    console.log(validUploadedImages);  // right images get printed

    handleImagesUpload(validUploadedImages);
  };

  return (
    <div>
      <section>
        
        <p className={styles.dropzoneAreaSubtext}>
          Supported formats: JPEG and PNG
        </p>

        <input
          {...register('images')}
          id="image-input"
          multiple
          type="file"
          accept="image/jpeg, image/png"
          title="Upload image"
          onChange={handleChange}
          className={styles.dropzoneAreaInput}
        />
      </section>

      <div className={styles.dropzoneSubtitleWrapper}>
        {uploadedImages.length > 0 && (
          <p className={styles.dropzoneSubtitle}>
            Uploaded {uploadedImages.length} photos
          </p>
        )}

        {isErrorFilesLimit && (
          <p className={styles.dropzoneError} data-testid="filesLimitExceeded">
            <span className={styles.dropzoneErrorHeadline}>
              You reached the limit of {maxFiles} photos. Remove a photo to
              upload a new one.
            </span>
          </p>
        )}

        {errorFileName && (
          <p className={styles.dropzoneError}>
            <span className={styles.dropzoneErrorHeadline}>
              Image file name already exists:
            </span>
            <br />
            {errorFileName}
          </p>
        )}

        {errorFileType && (
          <p className={styles.dropzoneError}>
            <span className={styles.dropzoneErrorHeadline}>
              Unsupported format:
            </span>
            <br />
            {errorFileType}
          </p>
        )}

        {errorFileSize && (
          <p className={styles.dropzoneError}>
            <span className={styles.dropzoneErrorHeadline}>
              Maximum file size (10MB) exceeded:
            </span>
            <br />
            {errorFileSize}
          </p>
        )}
      </div>
    </div>
  );
};

export default ImageUpload;

I left out all imports, but other than this I kept everything;

We used the spread operator, concat() and pushed to the local state already, but somehow the merging does not work.

2

Answers


  1. Chosen as BEST ANSWER

    The problem is that the FileReader is asynchronous. That means we had to rebuild some things:

    We don't push the constructedFile, but now a simple object with needed properties to the validUploadedImages, because we want to read the file as dataURL only, when it has arrived at the parent component. Then we do the transformation from the type File to dataUrl, which is needed to display the images properly. This, we had to await and make the handleImagesUpload function async.

    // Inside EntryEditPage:

    ...

    const getDataUrlForFile = (file) => {
        return new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onload = () => {
            resolve(reader.result);
          };
          reader.onerror = reject;
          reader.readAsDataURL(file);
        });
    };
    
    const handleImagesUpload = async (uploadedImages) => {
        const extendedImages = [];
        
        for (const file of uploadedImages) {
          extendedImages.push({
            ...file,
            url: await getDataUrlForFile(file.file),
          });
        }
    
        setCurrentImages((prevState) => [...prevState, ...extendedImages]);
    };
    

    // In the child component: // changed...

    // FILE checked and OK
      const reader = new FileReader();
      reader.readAsDataURL(file);
    
      reader.onload = () => {
        const constructedFile = {
          name: file.name,
          url: reader.result,
          file,
        };
    
        validUploadedImages.push(constructedFile);
      };
    

    // to...

    // FILE checked and OK
      validUploadedImages.push({
        name: file.name,
        url: null,
        file,
      });
    

  2. setCurrentImages is a method to set the value of the state. You need such as:

    setCurrentImages([...currentImages, ...uploadedImages]);
    

    But, the method handleImagesUpload may not take currentImages from the outside of the function. Thus, you can give the currentImages as a parameter such as currentState.

    const EntryEditPage = () => {
    
    const [currentImages, setCurrentImages] = useState([]);
    
    const handleImagesUpload = (currentState, uploadedImages) => {
        console.log('handleImagesUpload -- curIm:', currentImages);  // output: right image(s)  
        console.log('handleImagesUpload -- uplIm:', uploadedImages); // output: right image(s)
        console.log('handleImagesUpload -- expected result:', [
          ...currentState,
          ...uploadedImages,
        ]);                                                          // output: []
    
        setCurrentImages([...currentState, ...uploadedImages]);
    };
    

    And return part can be:

    return (
        <section>
            <ImageUpload
              currentImages={currentImages}
              handleImagesUpload={(images) => handleImagesUpload(currentImages, images)}
            />
    
            <section>
              {currentImages.map((image) => (
                <div
                  key={image.name}
                >
                  <img src={image.url} alt={image.name} />
                  />
                </div>
              ))}
            </section>
         </section>
    
    )
    

    Or:

    return (
        <section>
            <ImageUpload
              currentImages={currentImages}
              handleImagesUpload={(images, currentImages) => handleImagesUpload(currentImages, images)}
            />
    
            <section>
              {currentImages.map((image) => (
                <div
                  key={image.name}
                >
                  <img src={image.url} alt={image.name} />
                  />
                </div>
              ))}
            </section>
         </section>
    
    )
    

    In the second one, you should also update the child component.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search