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
| Option | Applies to | Effect |
|---|---|---|
accept | MIME or extension | 'image/*', 'image/png', '.pdf' |
maxFileSize / minFileSize | Bytes | file-too-large / file-too-small |
maxFiles | Count | Includes files already in the uploader |
validator | Custom | Return 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.
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
isFileAccepted(file, ['image/*', '.pdf', 'application/json']);| Pattern | Matches |
|---|---|
image/png | Exact MIME type |
image/* | Any type whose MIME starts with image/ |
.pdf | Filename 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:
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
{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:
| Code | When |
|---|---|
file-invalid-type | MIME / extension mismatch |
file-too-large | Over maxFileSize |
file-too-small | Under minFileSize |
too-many-files | Would exceed maxFiles |
validation-error | Custom 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.