Back

Erstellen einer benutzerdefinierten Datei-Upload-Komponente für React

Erstellen einer benutzerdefinierten Datei-Upload-Komponente für React

Das native Datei-Input-Element funktioniert zwar, ist aber unattraktiv, unflexibel und bietet keinerlei Feedback während des Uploads. Die meisten Entwickler greifen auf Bibliotheken wie react-dropzone zurück, um dieses Problem zu lösen. Wenn Sie jedoch die volle Kontrolle möchten – oder einfach weniger Abhängigkeiten – ist das Erstellen einer benutzerdefinierten React-Datei-Upload-Komponente nicht schwer, sobald Sie die Einschränkungen verstehen.

Dieser Leitfaden behandelt die wesentlichen Aspekte: barrierefreie Dateiauswahl, optional Drag-and-Drop-Funktionalität, Validierung, Vorschaubilder, Fortschrittsanzeige und Abbruchmöglichkeit. Alles mit modernen funktionalen Komponenten und Hooks.

Wichtigste Erkenntnisse

  • Datei-Inputs sind in React aufgrund von Browser-Sicherheitsbeschränkungen, die eine programmatische Wertzuweisung verhindern, grundsätzlich unkontrolliert
  • Kombinieren Sie clientseitige Validierung immer mit serverseitiger Validierung, da MIME-Typen und das accept-Attribut umgangen werden können
  • Verwenden Sie XMLHttpRequest anstelle von fetch für eine zuverlässige Upload-Fortschrittsanzeige
  • Widerrufen Sie Object-URLs, die für Dateivorschauen erstellt wurden, um Speicherlecks zu vermeiden
  • Drag-and-Drop sollte eine progressive Verbesserung sein, kein Ersatz für Tastatur- und Klick-Interaktionen

Warum Datei-Inputs unkontrolliert bleiben

Hier ist etwas, das React-Entwickler stolpern lässt: Sie können den Wert von <input type="file"> nicht programmatisch setzen. Die Browser-Sicherheit verhindert dies. Das bedeutet, dass Datei-Inputs grundsätzlich unkontrolliert sind – React kann von ihnen lesen, aber niemals in sie schreiben.

Das gilt auch für React 19 Form Actions. Während useFormStatus den Pending-Status verfolgen kann, sind Server Actions nicht für große Datei-Uploads konzipiert. Sie werden Dateien weiterhin clientseitig verarbeiten, bevor Sie sie irgendwohin senden.

Die Grundlage: barrierefreie Dateiauswahl

Beginnen Sie mit dem Input selbst. Verstecken Sie ihn visuell, aber halten Sie ihn barrierefrei:

const FileUploader = () => {
  const inputRef = useRef<HTMLInputElement>(null)
  const [files, setFiles] = useState<File[]>([])

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      setFiles(Array.from(e.target.files))
    }
  }

  return (
    <div>
      <input
        ref={inputRef}
        type="file"
        id="file-upload"
        className="sr-only"
        onChange={handleChange}
        multiple
        aria-describedby="file-upload-help"
      />
      <label htmlFor="file-upload" className="upload-button">
        Select files
      </label>
      <span id="file-upload-help" className="sr-only">
        Accepted formats: images, PDFs up to 10MB
      </span>
    </div>
  )
}

Das accept-Attribut ist nur beratend – Benutzer können es umgehen. Validieren Sie immer serverseitig.

Drag-and-Drop als progressive Verbesserung hinzufügen

Die Drag-and-Drop-Datei-Upload-Funktionalität verbessert die Benutzererfahrung, ist aber nicht essentiell. Behalten Sie Tastatur- und Klick-Interaktionen als primären Pfad bei:

const [isDragging, setIsDragging] = useState(false)

const handleDragOver = (e: React.DragEvent) => {
  e.preventDefault()
  setIsDragging(true)
}

const handleDragLeave = () => setIsDragging(false)

const handleDrop = (e: React.DragEvent) => {
  e.preventDefault()
  setIsDragging(false)
  
  const droppedFiles = Array.from(e.dataTransfer.files)
  setFiles(droppedFiles)
}

Hinweis: DataTransferItem.getAsFileSystemHandle() bietet Verzeichniszugriff in modernen Browsern, aber die Unterstützung bleibt begrenzt. Bleiben Sie bei dataTransfer.files für breite Kompatibilität.

Clientseitige Validierung

Validieren Sie vor dem Upload, um die Benutzererfahrung zu verbessern:

const validateFile = (file: File): string | null => {
  const maxSize = 10 * 1024 * 1024 // 10MB
  const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
  
  if (file.size > maxSize) return 'File exceeds 10MB limit'
  if (!allowedTypes.includes(file.type)) return 'Invalid file type'
  return null
}

Denken Sie daran: MIME-Typen können gefälscht werden. Serverseitige Validierung ist obligatorisch.

Dateivorschauen mit ordnungsgemäßer Bereinigung

Für Bildvorschauen erstellt URL.createObjectURL temporäre URLs. Widerrufen Sie diese, um Speicherlecks zu vermeiden:

const [preview, setPreview] = useState<string | null>(null)

useEffect(() => {
  if (files[0]?.type.startsWith('image/')) {
    const url = URL.createObjectURL(files[0])
    setPreview(url)
    return () => URL.revokeObjectURL(url)
  }
}, [files])

Fortschrittsanzeige und Abbruch

Hier ist die unbequeme Wahrheit: fetch unterstützt Upload-Fortschritt nicht zuverlässig. Für echte Fortschrittsanzeige verwenden Sie XMLHttpRequest:

const [progress, setProgress] = useState(0)
const xhrRef = useRef<XMLHttpRequest | null>(null)

const upload = (file: File) => {
  const xhr = new XMLHttpRequest()
  xhrRef.current = xhr
  
  xhr.upload.onprogress = (e) => {
    if (e.lengthComputable) {
      setProgress(Math.round((e.loaded / e.total) * 100))
    }
  }
  
  const formData = new FormData()
  formData.append('file', file)
  
  xhr.open('POST', '/api/upload')
  xhr.send(formData)
}

const cancel = () => xhrRef.current?.abort()

Für große Dateien sollten Sie direkte Uploads zu Object-Storage (S3 Presigned URLs) oder Chunked-/Multipart-Ansätze in Betracht ziehen. Das Senden von mehrgigabyte-großen Dateien durch Ihren Anwendungsserver endet selten gut.

Fehlerzustände

Verfolgen Sie den Upload-Status explizit:

type UploadState = 
  | { status: 'idle' }
  | { status: 'uploading'; progress: number }
  | { status: 'success'; url: string }
  | { status: 'error'; message: string }

Diese diskriminierte Union macht das Rendering unkompliziert und typsicher.

Wann Sie stattdessen eine Bibliothek verwenden sollten

Das Erstellen Ihrer eigenen benutzerdefinierten Datei-Upload-React-Komponente macht Sinn, wenn Sie spezifisches Verhalten benötigen oder die Bundle-Größe minimieren möchten. Bibliotheken wie react-dropzone behandeln Randfälle, die Sie möglicherweise übersehen – aber sie fügen auch Abhängigkeiten und Meinungen hinzu, die Sie vielleicht nicht wollen.

Das Kernmuster ist einfach: versteckter Input, barrierefreies Label, optionale Drop-Zone, Validierung und XHR für den Fortschritt. Alles andere ist Verfeinerung.

Fazit

Eine benutzerdefinierte Datei-Upload-Komponente gibt Ihnen präzise Kontrolle über Verhalten, Styling und Bundle-Größe. Die wichtigsten Einschränkungen sind unkompliziert: Datei-Inputs müssen unkontrolliert bleiben, clientseitige Validierung ergänzt serverseitige Prüfungen, ersetzt sie aber niemals, und XMLHttpRequest bleibt die zuverlässige Wahl für Fortschrittsanzeige. Beginnen Sie mit barrierefreien Grundlagen, fügen Sie Drag-and-Drop als optionale Verbesserung hinzu und kümmern Sie sich um die Speicherbereinigung für Vorschauen. Ob Sie benutzerdefiniert entwickeln oder zu einer Bibliothek greifen, hängt von Ihren spezifischen Anforderungen ab – aber jetzt verstehen Sie, was unter der Haube passiert, so oder so.

FAQs

Die Browser-Sicherheit verhindert die programmatische Wertzuweisung bei Datei-Inputs. Zum Zurücksetzen rufen Sie entweder inputRef.current.value = '' mit einer Ref auf (was zum Löschen funktioniert, aber nicht zum Setzen), oder unmounten und remounten Sie den Input, indem Sie seine key-Prop ändern. Beide Ansätze löschen effektiv die Auswahl, während sie die Browser-Beschränkungen respektieren.

Erstellen Sie ein Array von Upload-Zuständen, einen pro Datei. Jedes Zustandsobjekt verfolgt seinen eigenen Fortschritt, Status und eventuelle Fehler. Verwenden Sie den Dateinamen oder eine generierte ID als Schlüssel. Beim Hochladen mappen Sie über die Dateien und erstellen separate XMLHttpRequest-Instanzen, wobei Sie den entsprechenden Zustandseintrag in jedem Fortschritts-Callback aktualisieren.

Das dragLeave-Event wird ausgelöst, wenn man sich zwischen Eltern- und Kindelementen bewegt. Beheben Sie dies, indem Sie prüfen, ob das related target innerhalb der Drop-Zone enthalten ist, bevor Sie isDragging auf false setzen, oder verwenden Sie einen Zähler, der bei dragEnter inkrementiert und bei dragLeave dekrementiert wird, wobei der Status nur aktualisiert wird, wenn der Zähler Null erreicht.

Verwenden Sie FormData für die meisten Fälle. Es verarbeitet Binärdaten effizient und unterstützt die multipart/form-data-Kodierung, die Server erwarten. Base64-Kodierung erhöht die Dateigröße um etwa 33% und erfordert mehr Verarbeitung auf beiden Seiten. Verwenden Sie base64 nur, wenn Ihre API es ausdrücklich erfordert oder wenn Sie kleine Dateien in JSON-Payloads einbetten.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay