Skip to content

Validation

Validation runs before a file enters the queue. Rejected files never become UploadFile entries; they show up on rejections (uploader) or onDropRejected (dropzone).

That split matters. Accepted files are the source of truth for progress and retry. Rejections are a one-shot report of the last batch — useful for a toast or inline error list, not for long-lived state.

Built-in rules

OptionApplies toEffect
acceptMIME or extension'image/*', 'image/png', '.pdf'
maxFileSize / minFileSizeBytesfile-too-large / file-too-small
maxFilesCountIncludes files already in the uploader
validatorCustomReturn a FileError, an array, or null

useUploader and useDropzone share the same validateFiles helper. Put the authoritative rules on the uploader so paste, a file-picker button, and drop all hit the same gate. Mirror accept (and optionally size) on the dropzone so drag feedback (isDragAccept / isDragReject) is accurate.

tsx
const ACCEPT = ['image/*', '.pdf'];
const MAX_SIZE = 10 * 1024 * 1024;

const uploader = useUploader({
  adapter,
  accept: ACCEPT,
  maxFileSize: MAX_SIZE,
  maxFiles: 8,
});

const dropzone = useDropzone({
  onDrop: uploader.addFiles,
  accept: ACCEPT,
  maxFileSize: MAX_SIZE,
});

Dropzone validation is a first filter. Uploader validation still runs inside addFiles, including maxFiles against files.length.

Accept patterns

ts
isFileAccepted(file, ['image/*', '.pdf', 'application/json']);
PatternMatches
image/pngExact MIME type
image/*Any type whose MIME starts with image/
.pdfFilename extension (case-insensitive)

An empty or omitted accept list accepts everything.

During drag, the dropzone can only see MIME types from dataTransfer.items, not filenames. Extension-only rules (.pdf) will not produce reliable drag reject/accept highlighting. They still apply on drop and on file-dialog select.

Custom validators

Return null to accept, or a FileError (or array) to reject:

ts
const noSpaces: FileValidator = (file) => {
  if (file.name.includes(' ')) {
    return {
      code: 'validation-error',
      message: 'Filename must not contain spaces',
    };
  }
  return null;
};

const uploader = useUploader({
  adapter,
  validator: noSpaces,
});

Built-in checks run first; custom errors are appended. A file can carry multiple errors (wrong type and too large).

Keep validators pure and cheap. They run on every add. Do not hit the network here — that belongs in the adapter.

Reading rejections

tsx
{uploader.rejections.length > 0 && (
  <ul>
    {uploader.rejections.map((rejection, i) => (
      <li key={`${rejection.file.name}-${i}`}>
        {rejection.file.name}: {rejection.errors.map((e) => e.message).join(', ')}
      </li>
    ))}
  </ul>
)}

rejections is replaced on every addFiles call, not accumulated. If you need a history, copy them in onDropRejected or after addFiles.

Error codes:

CodeWhen
file-invalid-typeMIME / extension mismatch
file-too-largeOver maxFileSize
file-too-smallUnder minFileSize
too-many-filesWould exceed maxFiles
validation-errorCustom validator (by convention)

You can use other strings in a custom FileError.code; the union is for the built-in set.

maxFiles across sources

maxFiles on the uploader counts already tracked files plus the new batch. Dropzone maxFiles only sees the files in that drop/select event. Prefer enforcing the cap on the uploader so paste + drop + button cannot overshoot.

Released under the MIT License.