← Back to list

File uploader component for React

The file upload component works for uploading single or multiple files.

Yunuen Moncada · 2024-08-07 05:29 · 0 claps · 9.8 min read
#react #image-uploader #multiple-image-upload
Open on Medium ↗
Wiki topics: 🌐 · Web Development

File uploader component for React

The file upload component works for uploading single or multiple files.

By default we can upload any type of file, without restrictions on format, file size and in case of an image; without restriction of dimensions.

The first part is the declaration of our functional component, along with its properties:

export const FileUploader = ({
      onFilesSelected,
      validateDimensions= false,
      dimensions = {
          width: 0,
          height: 0
      },
      updateFiles = [],
      acceptFiles = [],
      multipleFiles = false,
      maxFiles = 1,
      fieldName,
      deleteFiles,
      maxSizeFile = 0
  }) => {
    const [files, setFiles] = useState([]);
    const inputFileRef = useRef();
    const [formatFiles, setFormatFiles] = useState([]);
    const [formatImages] = useState(['png','webp','jpeg','jpg','svg','webp']);
    const [styleOndrag, setStyleOnDrag] = useState('');
    const [validateImgDimensions, setValidateImgDimensions] = useState(false);
    const [lengthCurrentFiles, setLengthCurrentFiles] = useState(0);
    const [maxSizeFiles, setMaxSizeFiles] = useState(maxSizeFile);
    const [isMultiple, setIsMultiple] = useState(false);
    const [maxSize, setMaxSize] = useState(0);

    return (<div></div>);
}

Also, we extract in an unstructured way the props that we will be able to receive from the parent component, the props with initial value are not necessary to pass them for our component to work.

We will also add an extra function that we will use later to convert the size of our files to any format in bytes.

export const bytesToSize = (bytes, seperator = "") => {
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
    if (bytes === 0) return 'n/a'
    const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10)
    if (i === 0) return `${bytes}${seperator}${sizes[i]}`
    return `${(bytes / (1024 ** i)).toFixed(1)} ${seperator}${sizes[i]}`
}

The 3 essential functions are those that help us to upload our files by uploading a single file, a batch of files and to update the parent component.

const handleFileChange = (e) => {
    e.preventDefault();
    setStyleOnDrag('')
    let newFile;

    if(e.dataTransfer) newFile = e.dataTransfer.files[0];
    else newFile = e.target.files[0];

    const fileType = newFile.type?.split("/")[1] ?? '';
    let validateSizeFile, validateDimensionsImage = true, validatedFileType = false;

    validateSizeFile = validateFileSize(newFile);

    if(validateSizeFile && acceptFiles.length > 0) {
        if (formatFiles.includes(fileType) && !formatImages.includes(fileType) || formatFiles.includes('video') || formatFiles.includes('audio')) {
            validatedFileType = true;
            validateDimensionsImage = true;
        } else if(validateDimensions && formatFiles.includes('image') || validateDimensions && formatImages.includes(fileType)) {
            validateImageDimensions(newFile);
            validateDimensionsImage = validateImgDimensions;
            validatedFileType = true;
        } else if(!(validateDimensions) && formatFiles.includes('image') || !(validateDimensions) && formatImages.includes(fileType)) {
            if(formatImages.includes(fileType)) {
                validateDimensionsImage = true;
                validatedFileType = true;
            }
        }
    } else {
        validateDimensionsImage = true;
        validatedFileType = true;
    }

    const data = {
        file: newFile,
        approve: (validateSizeFile && validateDimensionsImage && validatedFileType)
    };

    handleSetFiles(data);
}

const handleFilesChange = (e) => {
    e.preventDefault();
    setStyleOnDrag('');
    let newFiles;
    let validateSizeFile = false, validateDimensionsImage = true, validatedFileType = false;

    if(e.dataTransfer) newFiles = Array.from(e.dataTransfer.files);
    else newFiles = Array.from(e.target.files)

    const data = newFiles.map(currentFile => {
        const fileType = currentFile.type?.split("/")[1] ?? '';

        validateSizeFile = validateFileSize(currentFile);

        if(validateSizeFile && acceptFiles.length > 0) {
            if (formatFiles.includes(fileType) && !formatImages.includes(fileType) || formatFiles.includes('video') || formatFiles.includes('audio')) {
                validatedFileType = true;
                validateDimensionsImage = true;
            } else if(validateDimensions && formatFiles.includes('image') || validateDimensions && formatImages.includes(fileType)) {
                validateImageDimensions(currentFile);
                validateDimensionsImage = validateImgDimensions;
                validatedFileType = true;
            } else if(!(validateDimensions) && formatFiles.includes('image') || !(validateDimensions) && formatImages.includes(fileType)) {
                if(formatImages.includes(fileType)) {
                    validateDimensionsImage = true;
                    validatedFileType = true;
                }
            }
        } else {
            validateDimensionsImage = true;
            validatedFileType = true;
        }

        return {
            file: currentFile,
            approve: (validateSizeFile && validateDimensionsImage && validatedFileType)
        }
    });

    handleSetFiles(data)
}

const handleSetFiles = (data) => {
    if(multipleFiles) {
        const newFilesSelected = [];

        if(files.length > 0) {
            files.forEach(item => {
                if(item.approve === true) newFilesSelected.push(item.file)
            })
        }

        data.forEach(item => {
            if(newFilesSelected.length < maxFiles)
                setFiles((prevFiles) => [...prevFiles, item]);
            if(item.approve === true && newFilesSelected.length < maxFiles)
                newFilesSelected.push(item.file);
        });

        onFilesSelected(newFilesSelected);
        cleanInput();
    }
    if(!(multipleFiles)) {
        setFiles([data]);
        if(data.approve) onFilesSelected(data.file);
        cleanInput();
    }
}

The following functions to be described complement the previous code.

cleanInput(), we use it to clean our input file of the files previously uploaded, and to be able to upload new files, even the same one we just uploaded.

validateFileSize(), receives a file in blob format, and validates the size as true if it meets the maximum allowed weight per file, even if no maximum weight is set.

validateImageDimensions(), validates the dimensions of an image type file and returns true if the dimensions meet the set parameters.

const cleanInput = () => inputFileRef.current.value = null;

const validateFileSize = (f) => {
    const sizeCurrentFile = Math.round(f.size / 1024);
    return (maxSize === 0) ? true : (sizeCurrentFile <= maxSize);
}

const validateImageDimensions = (f) => {
    const currentImg = URL.createObjectURL(f)
    const img = document.createElement("img");
    img.src = currentImg;
    img.onload = () => setValidateImgDimensions(!(img.width > dimensions.width || img.height > dimensions.height))
}

The remaining code complements the functionality of our component.

const handleRemoveFile = (index,files,file) =>  {
    const newFiles = files.filter((_, i) => i !== index);
    setFiles(newFiles);
    deleteFiles(newFiles.filter(item => item.approve === true), file);
    if(newFiles.length === 0) cleanInput();
};

const handleOnDragEnter = (e) => {
  e.preventDefault();
  setStyleOnDrag('-on-drag')
}

const handleOnDragLeave = (e) => {
    e.preventDefault();
    setStyleOnDrag('')
}

const handleAcceptedFiles = () => {
    const filesFiltered = acceptFiles.filter(item => item?.split("/")[1] !== '*');
    const allFilesFiltered = acceptFiles.filter(item => item?.split("/")[1] === '*');
    const filesType = filesFiltered.map(item => item?.split("/")[1]);
    const allFilesType = allFilesFiltered.map(item => item?.split("/")[0]);

    setFormatFiles([...filesType, ...allFilesType]);
};

useEffect(() => {
    setLengthCurrentFiles(files.filter(item => item.approve === true).length)
}, [files]);

useEffect(() => {
    const maxSize = (maxSizeFiles * 1024);
    setMaxSize(maxSize);
}, [maxSizeFiles])

useEffect(() => {
    setMaxSizeFiles(maxSizeFile);
}, [maxSizeFile]);

useEffect(() => {
    setIsMultiple(multipleFiles);
}, [multipleFiles]);

useEffect(() => {
    if(updateFiles.length > 0) setFiles(updateFiles);
}, [updateFiles])

useEffect(() => {
    handleAcceptedFiles();
}, [acceptFiles]);

handleRemoveFile(), receives 3 parameters, this function is also sent to the parent component if needed. The first index value allows us to delete the file we are indicating inside our component. The second one is used to update our array of files, like those of the parent component. And the third parameter is the file that we have just deleted, that we only send it to its parent component, in case it is necessary to use it.

Finally we have a condition to clean our input, in case our file array remains at 0.

handleOnDragEnter() and handleOnDragLeave() only help us to show the focus effect when we drag a file over our container div.

handleAcceptedFiles(), is the function that filters the formats to be validated if necessary.

The formatFiles status will always contain the formats to be validated. If the format is general; it will know if the format has the special character *. That will only happen when we indicate that we want to upload any image, video or audio.

The last code to add to the component is our jsx code after opening the return function.

<section className={`file-uploader__container`}>
    <div
        className={`file-uploader__uploader ${files.length > 0 && ("active")} ${styleOndrag}`}
        onDrop={(e) => {
            (!(multipleFiles) && files.length < maxFiles)  ? handleFileChange(e)  :
            ((multipleFiles   && files.length < maxFiles)  ? handleFilesChange(e) : '')
        }}
        onDragOver={(e) => handleOnDragEnter(e)}
        onDragLeave={(e) => handleOnDragLeave(e)}
    >
        <div className="file-uploader__uploader__info">
            {((!(multipleFiles) && lengthCurrentFiles === 0) || (multipleFiles && lengthCurrentFiles < maxFiles)) ?
                <>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
                        <path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128l-368 0zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39L296 392c0 13.3 10.7 24 24 24s24-10.7 24-24l0-134.1 39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z"/>
                    </svg>
                    <div>
                        <p><span>Arrastra y suelta</span> tus archivos aquí</p>
                        {
                            (maxSize > 0) && (
                                <p>Límite de {maxSizeFiles}MB por archivo.</p>
                            )
                        }
                        {
                            (acceptFiles.length > 0) && (
                                <p>Archivos permitidos: {<span className={'file-format'}>{formatFiles.join(' ')}</span>}</p>
                            )
                        }
                        {
                            (validateDimensions) && (<p>Dimensiones de imagen: {dimensions.width}x{dimensions.height}</p>)
                        }
                    </div>
                </>
                :
                <>
                    <div className="check-container">
                        <div className="check-container__checkmark">
                            <svg viewBox="0 0 65 51" fill="none" xmlns="http://www.w3.org/2000/svg">
                                <path
                                    d="M7 25L27.3077 44L58.5 7"
                                    stroke="white"
                                    strokeWidth="13"
                                    strokeLinecap="round"
                                    strokeLinejoin="round"
                                />
                            </svg>
                        </div>
                    </div>
                    <div>
                        <p>Límite de archivos alcanzado</p>
                    </div>
                </>
            }
        </div>

        <input
            ref={inputFileRef}
            type="file"
            hidden
            id={`${fieldName}`}
            onChange={(e) => {
                (multipleFiles) ?  handleFilesChange(e) : handleFileChange(e)
            }}
            accept={acceptFiles}
            name={`${fieldName}`}
            multiple={isMultiple}
        />

        {
            (((isMultiple && lengthCurrentFiles < maxFiles) || !(isMultiple) && lengthCurrentFiles === 0)) &&
            <label htmlFor={`${fieldName}`} className="btn-browser">
                {
                    (formatFiles.length === 0 && !(isMultiple)) ? 'Buscar imagen' :
                        (formatFiles.length > 0 && !(isMultiple)) ?  'Buscar archivo' :
                            (isMultiple && formatFiles.length === 0) ? 'Buscar imágenes'  : 'Buscar archivos'
                }
            </label>
        }

        {files.length > 0 && (
            <div className="file-uploader__list">
                {files.map((item, index) => {
                    const fileSize = bytesToSize(item.file.size)
                    const fileType = item.file.type?.split("/")[1] ?? '';

                    return (
                        <Fragment key={index}>
                            <div className={`file-uploader__list-item ${item.approve ? 'success' : 'failed'}`}>
                                <span>
                                    {
                                        (item.approve) ?
                                            <svg
                                                className={`${item.approve && ('success')}`}
                                                xmlns="http://www.w3.org/2000/svg"
                                                viewBox="0 0 512 512"
                                            >
                                                <path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
                                            </svg>
                                            :
                                            <svg
                                                xmlns="http://www.w3.org/2000/svg"
                                                viewBox="0 0 512 512"
                                                className={`${!item.approve && ('failed')}`}
                                            >
                                                <path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
                                            </svg>
                                    }
                                </span>
                                {
                                    (formatImages.includes(fileType)) ?
                                        <Image
                                            width={35}
                                            src={URL.createObjectURL(item.file)}
                                        />
                                        :
                                        <span>
                                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
                                                <path d="M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128z"/>
                                            </svg>
                                        </span>
                                }
                                <div className="file-info">
                                    <p className={'name'}>{item.file?.name ?? item.file.fileName}</p>
                                    <p className={'size'}>{fileSize}</p>
                                </div>
                                <div className="file-uploader__actions">
                                    {
                                        (!formatImages.includes(fileType) && item.approve) && (
                                            <Link
                                                className={'btn-download'}
                                                href={URL.createObjectURL(item.file)}
                                                target={'_blank'}
                                                download={true}
                                            >
                                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                                                  <path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 242.7-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7 288 32zM64 352c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-101.5 0-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352 64 352zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/>
                                                </svg>
                                            </Link>
                                        )
                                    }
                                    <button
                                        type={'button'}
                                        className={'btn-trash'}
                                        onClick={() => handleRemoveFile(index,files,item)}
                                    >
                                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
                                            <path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/>
                                        </svg>
                                    </button>
                                </div>
                            </div>
                        </Fragment>
                    )
                })}
            </div>
        )}

        {
            (isMultiple) && (
                <p>
                    <strong>{lengthCurrentFiles} / {maxFiles} {' '}</strong>
                    {lengthCurrentFiles > 1 && ('archivos seleccionados')}
                    {lengthCurrentFiles === 1 && ('archivo seleccionado')}
                </p>
            )
        }
    </div>
</section>

The css code is as follows:

:root {
  --black: #000;
  --dark: #3d3d3d;
  --white: #fff;
  --green: #6dc24b;
  --red: #d44;
  --border-radius: 8px;
  --light: #fefefe;
}

.file-uploader {
  &__container {
    width: 100%;
    transition: .2s ease-in-out;
  }
  &__uploader {
    width: 100%;
    border: 1px dashed lightgray;
    background: var(--white);
    padding: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 16px;
    position: relative;
    border-radius: var(--border-radius);
    transition: .2s ease-in-out;
    p {
      color: var(--black);
    }
    &.-on-drag {
      background: var(--light);
      border-color: var(--green);
    }
    &.active {
      border-color: var(--black);
    }
    &__info {
      display: flex;
      align-items: center;
      gap: 16px;
      transition: .2s ease-in-out;
      svg {
        width: 32px;
      }
      div {
        p {
          margin: 0;
          font-size: 14px;
          span {
            font-weight: 700;
          }
        }
      }
      .check-container {
        width: 40px;
        display: flex;
        flex-flow: column;
        align-items: center;
        justify-content: center;
        &__checkmark {
          width: 100%;
          height: 40px;
          background: var(--green);
          transform: scale(0.84);
          border-radius: 50%;
          animation: animateContainer 0.3s ease-out forwards .3s;
          display: flex;
          align-items: center;
          justify-content: center;
          opacity: 0;
          svg {
            width: 65%;
            stroke-dasharray: 80;
            stroke-dashoffset: 80;
            animation: animateCheck .5s forwards .5s ease-out;
          }
        }
        @keyframes animateContainer {
          0% {
            opacity: 0;
            transform: scale(0);
          }
          25% {
            opacity: 1;
            transform: scale(0.9);
          }
          43.75% {
            transform: scale(1.15);
          }
          62.5% {
            transform: scale(1);
          }
          81.25% {
          }
          100% {
            opacity: 1;
          }
        }
        @keyframes animateCheck {
          from {
            stroke-dashoffset: 80;
          }
          to {
            stroke-dashoffset: 0;
          }
        }
      }
    }
    .btn-browser {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.5rem 1rem;
      color: var(--white);
      border: 1px solid var(--black);
      border-radius: 8px;
      cursor: pointer;
      background-color: var(--black);
      transition: .25s ease-in-out;
      &:hover {
        background-color: var(--black);
        transform: scale(1.05);
      }
    }
    .success-file {
      display: flex;
      align-items: center;
      color: #6dc24b;
      transition: .2s ease-in-out;
      p {
        margin: 0;
        font-size: 14px;
        font-weight: bold;
      }
    }
    input[type="file"] {
      display: none !important;
    }
  }
  &__list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 100%;
    overflow: auto;
    max-height: 290px;
    padding-right: .25rem;
    transition: .2s ease-in-out;
    &-item {
      width: 100%;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0.1rem .25rem;
      min-height: 50px;
      border: 0;
      background: rgba(0,0,0,.03);
      border-radius: 4px;
      span {
        svg {
          width: 20px;
          fill: rgba(0,0,0,.5);
          margin-right: .5rem;
          &.success {
            fill: var(--green);
          }
          &.failed {
            fill: var(--red);
          }
        }
      }
      .ant-image {
        margin-right: .5rem;
      }
      .file-info {
        width: 100%;
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
        flex: 1;
        p {
          margin: 0;
          font-size: 14px;
          color: #333;
          line-height: 14px;
          &.name {
            width: 300px;
            text-overflow: ellipsis;
            overflow: hidden;
            white-space: nowrap;
          }
          &.size {
            font-weight: bold;
            font-size: 12px;
          }
        }
      }
      .file-actions {
        cursor: pointer;
        svg {
          font-size: 18px;
          color: #888;
        }
        &:hover {
          svg {
            color: var(--red);
          }
        }
      }
      &.failed {
        border: .1rem solid rgba(0,0,0,.05);
      }
      &.success {
        border: .1rem solid rgba(0,0,0,.05);
      }
    }
  }
  &__actions {
    width: max-content;
    display: flex;
    gap: 8px;
    align-items: center;
    .btn-download {
      width: 25px;
      height: 25px;
      background: rgba(0,0,0,0);
      display: flex;
      align-items: center;
      justify-content: center;
      svg {
        width: 1rem;
        height: 1rem;
        fill: var(--black);
      }
      &:hover {
        cursor: pointer;
        svg {
          fill: var(--green);
        }
      }
    }
    .btn-trash {
      background: rgba(0,0,0,0);
      border: 0;
      width: 25px;
      height: 25px;
      display: flex;
      align-items: center;
      justify-content: center;
      svg {
        width: 1rem;
        height: 1rem;
        fill: var(--black);
      }
      &:hover {
        cursor: pointer;
        svg {
          fill: var(--red);
        }
      }
    }
  }
}

@media (prefers-color-scheme: dark) {
  .input-file {
    &__uploader {
      border-color: var(--dark);
      background: var(--black);
      p {
        color: var(--white);
      }
      &.-on-drag {
        background: var(--dark);
      }
      &.active {
        border-color: var(--white);
      }
      &__info {
        color: var(--white);
        svg {
          fill: var(--white);
        }
      }
      .btn-browser {
        color: var(--black);
        border: 1px solid var(--black);
        background-color: var(--white);
        &:hover {
          background-color: var(--white);
        }
      }
    }
    &__list {
      &-item {
        background: var(--dark);
        svg {
          fill: var(--black);
        }
        .file-info {
          p {
            color: var(--white);
          }
        }
        span {
          svg {
            fill: var(--black);
          }
        }
      }
    }
    &__actions {
      .btn-download {
        svg {
          fill: var(--white)
        }
      }
      .btn-trash {
        svg {
          fill: var(--white);
        }
        &:hover {
          svg {
            fill: var(--red);
          }
        }
      }
    }
  }
}

메타데이터
post_id
2a7f10f6ca38
slug
file-uploader-component-for-react-2a7f10f6ca38
url
https://medium.com/@moncadayunuen/file-uploader-component-for-react-2a7f10f6ca38
canonical_url
https://medium.com/@moncadayunuen/file-uploader-component-for-react-2a7f10f6ca38
author_url
https://medium.com/@moncadayunuen
status
ok
fetched_at
2026-07-15 07:20:45