hooks/useUploader.js

import { useRef, useEffect, MutableRefObject } from 'react';

import { errorMessages, defaultUploadConfig } from '../constants';

const { defaultIsMultiple, defaultMaxSize } = defaultUploadConfig;

/** @module Uploader */

/**
 * Object for inserting to html element
 * @interface
 * @global
 * @typedef UploadRefContainer
 * @property {MutableRefObject<HTMLInputElement>} ref - Reference to html element.
 */

/**
 * Object to manage uploading files
 * @interface
 * @global
 * @typedef UploadManager
 * @property {UploadRefContainer} uploader - Object for inserting to html element.
 * @property {function} reset - Function to reset value of input.
 */

/**
 * Uploading config
 * @interface
 * @global
 * @typedef UploadConfig
 * @property onSelectFile {function} - Function is called after uploading with uploaded file. Required
 * @property onError {function} - Function is called after failed downloading. Optional
 * @property isMultiple {boolean} - Indicator for count of uploaded files. Optional. Default - true
 * @property maxFiles {number} - Limit for maximum selected files. Optional.
 * @property maxSize {number} - Limit for maximum size of file. Optional. Default - 5242880 bytes (5MB)
 * @property validTypes {Array<string>} - Set of valid types for files. Optional.
 */

/**
 * Hook for uploading files
 * @param config {UploadConfig} - Uploading config.
 * @return {UploadManager} - Object to manage uploading files.
 * @function
 */
export function useUploader(
    { onSelectFile, onError, isMultiple = defaultIsMultiple, maxFiles, maxSize = defaultMaxSize, validTypes } = {}
) {

    /**
     * Reference to input
     */
    const inputRef = useRef(null);

    /**
     * Reset input value
     */
    const reset = () => {
        if (inputRef.current) inputRef.current.value = '';
    };

    /**
     * Handle error
     */
    const handleError = (message) => {
        reset();
        const error = new Error(message);
        if (onError && typeof onError === 'function') {
            return onError(new Error(message));
        }
    };

    /**
     * Check validity of files
     */
    const isInvalidType = files => {
        if (validTypes) {
            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                if (!validTypes.find(type => file.type.includes(type))) return file;
            }
        }
    };

    /**
     * Check max size of files
     */
    const isMaxSize = files => {
        if (maxSize) {
            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                if (file.size > maxSize) return file;
            }
        }
    };

    /**
     * Handler for input changes
     */
    const onChange = () => {
        const { files } = inputRef.current;
        if (files && files.length) {
            if (maxFiles && files.length > maxFiles) {
                return handleError(errorMessages.amount + maxFiles);
            }
            const fileWithMaxSize = isMaxSize(files);
            if (fileWithMaxSize) {
                return handleError(fileWithMaxSize.name + errorMessages.size + maxSize);
            }
            const fileWithInvalidType = isInvalidType(files);
            if (fileWithInvalidType) {
                return handleError(fileWithInvalidType.name + errorMessages.type + fileWithInvalidType.type);
            }
            const file = files[0];
            if (onSelectFile && typeof onSelectFile === 'function') {
                return onSelectFile(isMultiple ? files : file);
            }
        } else {
            return handleError(errorMessages.existingFile);
        }
    };

    /**
     * Hook for adding/removing input listener
     */
    useEffect(() => {
        const element = inputRef.current;
        if (element) {
            element.setAttribute('multiple', isMultiple);
            element.setAttribute('type', 'file');
            element.addEventListener('change', onChange);
        }
        return () => {
            if (element) element.removeEventListener('change', onChange);
        };
    }, [inputRef, isMultiple]); // eslint-disable-line

    return {
        uploader: { ref: inputRef },
        reset
    };

}