@quanticjs/react-files
v8.0.0
Published
QuanticJS file upload/download React components and hooks
Readme
@quanticjs/react-files
File upload/download React components and hooks for QuanticJS apps: presigned single-part and multipart uploads with progress, validation, and cancellation.
Installation
pnpm add @quanticjs/react-files @quanticjs/react-coreRequires @quanticjs/tailwind-preset >= 8 in the consuming app's CSS build — components render with v8 token utilities (shadow-* tiers, z-(--z-*), animate-*) that compile to nothing on older presets. See docs/MIGRATION-8.md.
Quick start
import { FilesProvider, FileUpload } from '@quanticjs/react-files';
function App() {
return (
<FilesProvider apiBaseUrl="/api/files">
<FileUpload
allowedTypes={['application/pdf', 'image/*']}
maxSize={10 * 1024 * 1024}
onUploadComplete={(ref) => console.log('uploaded', ref.id)}
onValidationError={(file, error) => console.warn(file.name, error.code, error.message)}
/>
</FilesProvider>
);
}Files above multipartThreshold (default 100 MB) automatically use the multipart flow.
Security guidance
Always set allowedTypes
The HTML accept attribute is a UI hint only — it filters the file picker but does nothing for drag-and-drop and is trivially bypassed. allowedTypes is enforced in JavaScript before any network request:
- Exact MIME strings (
application/pdf) or wildcards (image/*), case-insensitive. - The declared
file.typeand the file extension are both checked. Extensions map to MIME types through a built-in table (pdf, png, jpg/jpeg, gif, webp, csv, xlsx, docx, txt, zip); unknown extensions pass the extension check, but the MIME check still applies. - If the browser reports an empty
file.type, the extension table is the fallback; if the extension is unknown too, the file is rejected (INVALID_TYPE) — validation fails closed.
Client-side checks are defense-in-depth, not a boundary
Everything this package validates runs in the browser and can be bypassed by anyone talking to your API directly. Your server must re-validate type, size, and content on every upload. Treat allowedTypes/maxSize as a UX and first-line filter only.
Custom validation hook (AV pre-scan, magic bytes)
validateFile runs after the built-in checks and before any request. Return true to accept or a string to reject with that message (a thrown error also rejects, with code CUSTOM):
<FileUpload
allowedTypes={['application/pdf']}
validateFile={async (file) => {
// Magic-byte sniffing: a real PDF starts with %PDF
const head = new Uint8Array(await file.slice(0, 4).arrayBuffer());
if (String.fromCharCode(...head) !== '%PDF') return 'File is not a valid PDF';
// Or hand off to an AV pre-scan endpoint
const res = await fetch('/api/av/prescan', { method: 'POST', body: file.slice(0, 65536) });
if (!res.ok) return 'File failed the malware pre-scan';
return true;
}}
onUploadComplete={handleComplete}
/>Validation
Each rejected file fires onValidationError(file, { code, message }) once (falling back to onUploadError(message) when not provided) with one of:
| Code | Meaning |
|---|---|
| INVALID_TYPE | MIME/extension not in allowedTypes |
| TOO_LARGE | file.size exceeds maxSize |
| CUSTOM | validateFile returned a string or threw |
Rejection is a hard stop for that file — no request fires — but other files in the same batch still upload.
Messages are overridable for i18n via the messages prop:
<FileUpload
allowedTypes={['image/*']}
maxSize={5 * 1024 * 1024}
messages={{
invalidType: (file, allowed) => `${file.name}: nur ${allowed.join(', ')} erlaubt`,
tooLarge: (file, maxSize) => `${file.name} ist zu groß (max. ${maxSize} Bytes)`,
}}
onUploadComplete={handleComplete}
/>Cancellation
FileUpload shows a Cancel button while uploading. With the hook directly:
import { useFileUpload } from '@quanticjs/react-files';
function Uploader() {
const { upload, cancel, isUploading, progress } = useFileUpload({
allowedTypes: ['application/zip'],
onCancel: () => console.log('upload cancelled'),
});
// upload(file) rejects with UploadCancelledError after cancel() —
// not an error condition, and onUploadError-style handling should ignore it.
}- Cancelling aborts the in-flight request (
xhr.abort()/ fetch abort) and stops scheduling further multipart parts. - After cancel, state returns to idle and progress resets;
onCancelfires. Cancellation is not reported as an error. - If cancel arrives after the multipart complete call was already sent, it is too late — the upload finishes normally.
Multipart integrity
- Completed parts are sorted by
partNumberbefore thecompletecall, regardless of the order parts finish in. - Any failed part aborts the remaining parts and fails the whole upload — a partial
completeis never sent.
API
<FilesProvider apiBaseUrl multipartThreshold?>
Context provider required by all hooks/components.
<FileUpload /> props
| Prop | Type | Description |
|---|---|---|
| onUploadComplete | (file: FileReference) => void | Required; fires per uploaded file |
| onUploadError | (error: string) => void | Upload failures (and validation fallback) |
| onValidationError | (file: File, error: FileValidationError) => void | Per-file validation rejections |
| onCancel | () => void | After a cancel completes |
| allowedTypes | string[] | MIME whitelist (exact or type/*) |
| maxSize | number | Max bytes per file |
| validateFile | (file: File) => Promise<true \| string> | Custom async check |
| messages | UploadMessages | i18n overrides for validation messages |
| labels | Partial<FileUploadLabels> | Per-key overrides for rendered strings |
| accept / multiple / disabled / className | — | Standard input/picker options |
<FileList /> props
| Prop | Type | Description |
|---|---|---|
| onDelete / showDelete | — | Delete-action wiring |
| emptyMessage | string | Empty-state text; wins over labels.empty |
| labels | Partial<FileListLabels> | Per-key overrides for rendered strings |
| columns | array or function | Replace or extend the default columns |
useFileUpload(options?)
Returns { upload, cancel, isUploading, progress, error, reset }. Options: allowedTypes, maxSize, validateFile, messages, onCancel. messages resolves per key: hook option > provider catalog (files.messages) > built-in English.
Internationalization
All rendered strings live in labels interfaces with English defaults and resolve per key with the precedence explicit prop > provider catalog > default. App-wide injection goes through TranslationProvider from @quanticjs/react-ui — this package registers the files namespace (FilesTranslations):
import { TranslationProvider } from '@quanticjs/react-ui';
<TranslationProvider
locale="de-DE"
translations={{
files: {
upload: { preparing: 'Upload wird vorbereitet…' }, // Partial<FileUploadLabels>
list: { searchPlaceholder: 'Dateien durchsuchen…' }, // Partial<FileListLabels>
downloadLink: { download: 'Herunterladen' }, // Partial<FileDownloadLinkLabels>
messages: { tooLarge: (file) => `„${file.name}" ist zu groß` }, // Partial<UploadMessages>
},
}}
>
<App />
</TranslationProvider>FileUploadLabels:uploadFile,uploading(percentage),preparing,cancel,dropHint(multiple),maxSize(formattedSize)FileListLabels:empty, table headers (name,size,type,uploaded,actions),delete,deleteAriaLabel(fileName),searchPlaceholder,searchAriaLabel,loadError,retry,previous,next,pageStatus(page, totalPages)FileDownloadLinkLabels:download- Function-valued labels handle interpolation; JSON-based pipelines wrap them (
pageStatus: (p, t) => t('files.pageStatus', { p, t }))
The provider's locale also drives the formatBytes/formatDateTime output in FileList, the max-size hint in FileUpload, and the default too-large validation message — reactively, unlike setDefaultLocale.
Exports
FilesProvider, useFilesContext
useFileUpload, UploadValidationError, UploadCancelledError, type UseFileUploadOptions
useFileList
useFileDownloadUrl
FileUpload, type FileUploadProps
FileList, type FileListProps
FileDownloadLink
DEFAULT_UPLOAD_LABELS, DEFAULT_LIST_LABELS, DEFAULT_DOWNLOAD_LINK_LABELS
type FileUploadLabels, type FileListLabels, type FileDownloadLinkLabels, type FilesTranslations
type FileReference, type FileDto, type FileListResponse
type FileValidationCode, type FileValidationError
type UploadMessages, type UploadProgress, type DownloadUrlResponse, type FilesContextValueRTL & reduced motion
Components carry no physical direction utilities, so they render correctly under dir="rtl" with no configuration. The drop-zone hover transition and the upload progress bar carry motion-reduce:transition-none; the preset's theme.css additionally disables animation/transition durations globally under prefers-reduced-motion: reduce.
