How to use react-dropzone with react-hook-form?
Asked Answered
Y

4

11

How to use react-dropzone with react-hook-form so that the form returns proper file - not just file name?

Yacano answered 11/11, 2020 at 9:38 Comment(1)
there is a full article here: dev.to/vibhanshu909/…Franky
F
11

Here is a work from a react-hook-form Github discussion:

export const DropzoneField = ({
  name,
  multiple,
  ...rest
}) => {
  const { control } = useFormContext()

  return (
    <Controller
      render={({ onChange }) => (
        <Dropzone
          multiple={multiple}
          onChange={e =>
            onChange(multiple ? e.target.files : e.target.files[0])
          }
          {...rest}
        />
      )}
      name={name}
      control={control}
      defaultValue=''
    />
  )
}

const Dropzone = ({
  multiple,
  onChange,
  ...rest
}) => {

  const {
    getRootProps,
    getInputProps,
  } = useDropzone({
    multiple,
    ...rest,
  })

  return (
    <div {...getRootProps()}>
      <input {...getInputProps({ onChange })} />
    </div>
  )
}

You should check out Controller API as it was made to make integration with external controlled input easier. There are quite a few examples therein as well.

Franky answered 13/11, 2020 at 4:10 Comment(1)
onChange wasn't fired when I tried this out. I've resolved in using onDrop, see the discussion: github.com/react-dropzone/react-dropzone/pull/623Mcclure
Y
3

i did this way @Bill

const FileUpload = (props) => {
  const {
    control,
    label,
    labelClassName,
    name,
    isRequired,
    rules,
    error,
    multiple,
    maxFiles,
    setValue,
    accept,
    maxSize,
    setError,
    clearErrors,
    formGroupClassName,
    watch,
  } = props;
  const [files, setFiles] = useState(watch(name));
  const onDrop = useCallback(
    (acceptedFiles, rejectedFiles) => {
      if (rejectedFiles && rejectedFiles.length > 0) {
        setValue(name, []);
        setFiles([]);
        setError(name, {
          type: 'manual',
          message: rejectedFiles && rejectedFiles[0].errors[0].message,
        });
      } else {
        setFiles(
          acceptedFiles.map((file) =>
            Object.assign(file, {
              preview: URL.createObjectURL(file),
            }),
          ),
        );
        clearErrors(name);
        acceptedFiles.forEach((file) => {
          const reader = new FileReader();
          reader.onabort = () => toastError('File reading was aborted');
          reader.onerror = () => toastError('file reading has failed');
          reader.readAsDataURL(file);
          reader.onloadend = () => {
            setValue(name, file, { shouldValidate: true });
          };
        });
      }
    },
    [name, setValue, setError, clearErrors],
  );

  const deleteFile = (e, file) => {
    e.preventDefault();
    const newFiles = [...files];
    newFiles.splice(newFiles.indexOf(file), 1);
    if (newFiles.length > 0) {
      setFiles(newFiles);
    } else {
      setFiles(null);
      setValue(name, null);
    }
  };

  const thumbs =
    files &&
    files !== null &&
    files.map((file) => {
      const ext = file.name && file.name.substr(file.name.lastIndexOf('.') + 1);
      return ext === 'pdf' ? (
        <ul key={file.name} className="mt-2">
          <li>{file.name}</li>
        </ul>
      ) : (
        <div className="thumb position-relative" key={file.name}>
          <img src={file.preview ? file.preview : file} alt={file.name} />
          <Button
            className="trash-icon"
            color="danger"
            size="sm"
            onClick={(e) => deleteFile(e, file)}
          >
            <FontAwesomeIcon icon={faTrashAlt} size="sm" />
          </Button>
        </div>
      );
    });

  useEffect(() => {
    if (
      watch(name) !== '' &&
      typeof watch(name) === 'string' &&
      watch(name).startsWith('/')
    ) {
      setFiles([
        {
          preview: getFileStorageBaseUrl() + watch(name),
          name: watch(name)
            .substr(watch(name).lastIndexOf('/') + 1)
            .substr(0, watch(name).lastIndexOf('/')),
        },
      ]);
    }
  }, [watch, name]);
  useEffect(
    () => () => {
      if (files && files.length > 0) {
        files.forEach((file) => URL.revokeObjectURL(file.preview));
      }
    },
    [files],
  );

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    maxFiles: multiple ? maxFiles : 0,
    accept,
    onDrop,
    minSize: 0,
    maxSize,
    multiple,
  });
  return (
    <div className={formGroupClassName || 'file-input my-2 form-group'}>
      {label && (
        <label className={labelClassName} htmlFor={name}>
          {label}
          {isRequired && <span style={{ color: 'red' }}> * </span>}
        </label>
      )}
      <Controller
        control={control}
        name={name}
        rules={rules}
        render={(controllerProps) => (
          <div
            {...getRootProps({
              className: 'dropzone w-100 fs-20 d-flex align-items-center',
            })}
            {...controllerProps}
          >
            <input {...getInputProps()} />
            <FontAwesomeIcon
              icon={faCloudUploadAlt}
              size="sm"
              className="mr-1"
            />
            {isDragActive ? (
              <span className="fs-16">Drop the files here ... </span>
            ) : (
              <span className="fs-16">Select files </span>
            )}
          </div>
        )}
      />
      <aside className="thumbs-container">{thumbs}</aside>
      {error && <p className="form-error mb-0">{error.message}</p>}
    </div>
  );
};
Yacano answered 20/1, 2021 at 12:17 Comment(0)
V
3

here is the solution with v7

const DropzoneField = ({
  name,
  control,
  ...rest
}: {
  name: string;
  control: Control<FieldValues>;
}) => {
  // const { control } = useFormContext();

  return (
    <Controller
      render={({ field: { onChange } }) => (
        <Dropzone onChange={(e: any) => onChange(e.target.files[0])} {...rest} />
      )}
      name={name}
      control={control}
      defaultValue=""
    />
  );
};

const Dropzone = ({ onChange, ...rest }: { onChange: (...event: any[]) => void }) => {
  const onDrop = useCallback((acceptedFiles) => {
    // Do something with the files
    console.log({ acceptedFiles });
  }, []);
  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
  return (
    <div {...getRootProps()}>
      <input {...getInputProps({ onChange })} />
      {isDragActive ? (
        <p>Drop the files here ...</p>
      ) : (
        <p>Drag 'n' drop some files here, or click to select files</p>
      )}
    </div>
  );
};
Vengeance answered 25/7, 2021 at 13:44 Comment(2)
How do you manage errors on the dropzone itself with a form submit and no files added?Wilcher
Thanks for the example. A small aside is useCallback is unnecessary since you're not providing anything in the dependancy arrayValet
G
1

I got it working properly including with both drop and click to add a file using the following code:

FileInput.js

import React, { useCallback, useEffect } from "react"
import { useDropzone } from "react-dropzone"
import { useFormContext } from "react-hook-form"

const FileInput = props => {
    const { name, label = name } = props
    const { register, unregister, setValue, watch } = useFormContext()
    const files = watch(name)
    const onDrop = useCallback(
        droppedFiles => {
            setValue(name, droppedFiles, { shouldValidate: true })
        },
        [setValue, name]
    )
    const { getRootProps, getInputProps, isDragActive } = useDropzone({
        onDrop,
        accept: props.accept,
    })
    useEffect(() => {
        register(name)
        return () => {
            unregister(name)
        }
    }, [register, unregister, name])
    return (
        <>
            <label className=" " htmlFor={name}>
                {label}
            </label>
            <div
                {...getRootProps()}
                type="file"
                role="button"
                aria-label="File Upload"
                id={name}
            >
                <input {...props} {...getInputProps()} />
                <div
                    style={{ width: "500px", border: "black solid 2px" }}
                    className={" " + (isDragActive ? " " : " ")}
                >
                    <p className=" ">Drop the files here ...</p>

                    {!!files?.length && (
                        <div className=" ">
                            {files.map(file => {
                                return (
                                    <div key={file.name}>
                                        <img
                                            src={URL.createObjectURL(file)}
                                            alt={file.name}
                                            style={{
                                                height: "200px",
                                            }}
                                        />
                                    </div>
                                )
                            })}
                        </div>
                    )}
                </div>
            </div>
        </>
    )
}

export default FileInput

Form

import React from "react"
import { FormProvider, useForm } from "react-hook-form"
import FileInput from "./FileInput"

const Form = () => {
    const methods = useForm({
        mode: "onBlur",
    })
    const onSubmit = methods.handleSubmit(values => {
        console.log("values", values)
    })

    return (
        <FormProvider {...methods}>
            <form onSubmit={onSubmit}>
                <div className="">
                    <FileInput
                        accept="image/png, image/jpg, image/jpeg, image/gif"
                        name="file alt text"
                        label="File Upload"
                    />
                </div>
            </form>
        </FormProvider>
    )
}

export default Form
Genitals answered 21/4, 2022 at 9:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.