Skip to main content

useDropzone

WIP

Headless drag-and-drop and file-picker hook — prop getters, isDragging, accept filtering.

useDropzone is a headless hook that wires drag-and-drop and the native file picker to the upload store. It ships no markup and no styling — you get prop getters for a drop target and a hidden <input type="file">, plus a live isDragging flag. Everything visual is yours.

import { useDropzone } from '@gentleduck/upload/react'

function Drop() {
  const { getRootProps, getInputProps, isDragging, open } = useDropzone({
    purpose: 'attachment',
    accept: 'image/*,.pdf',
  })

  return (
    <div {...getRootProps()} data-active={isDragging}>
      <input {...getInputProps()} />
      <p>Drop files here or click to browse</p>
      <button type="button" onClick={open}>Choose files</button>
    </div>
  )
}

Dropped or selected files are dispatched to the store as addFiles (or handed to your own onFiles). The hook reads the store from <UploadProvider> context — or pass an explicit store.

Options

useDropzone<P>({
  purpose: P                          // forwarded to the addFiles command (required)
  meta?: Record<string, unknown>      // caller metadata forwarded to backend API calls
  accept?: string                     // "image/*,.pdf" — filters files and sets input accept
  multiple?: boolean                  // default true; false keeps only the first file
  disabled?: boolean                  // default false; no drop, no click-to-open
  noClick?: boolean                   // default false; don't open the picker on root click
  onFiles?: (files: File[]) => void   // override the default addFiles dispatch
  store?: UploadStore                 // dispatch target; falls back to context
})

accept matching

accept is a comma-separated list of tokens, matched like the native attribute:

TokenMatches
image/pngexact MIME type
image/*any type in the group
.pdffilename extension (case-insensitive)

A file passes if it matches any token. An absent or empty accept allows everything.

Return value

FieldTypePurpose
isDraggingbooleanTrue while files are dragged over the root
open() => voidProgrammatically open the file picker
inputRefRefObjectRef to the hidden input (already attached by getInputProps)
getRootProps(extra?) => propsSpread onto the drop target
getInputProps(extra?) => propsSpread onto the hidden <input type="file">

Both getters merge any handlers/styles you pass, calling yours first — so getRootProps({ onClick }) still runs your onClick before opening the picker.

Behavior notes

  • Nested children don't flicker — an internal depth counter tracks dragenter/dragleave, so isDragging stays true while hovering child elements.
  • drop is enabled — the root's dragover/drop call preventDefault, so the browser won't navigate away when a file is dropped.
  • Re-selecting the same file works — the input's value is reset after each change, so picking the same file twice still fires.
  • Filtering happens before dispatch — files rejected by accept never reach the store; with multiple: false only the first accepted file is dispatched.

Custom handling

Skip the store entirely and receive the raw file list:

const { getRootProps, getInputProps } = useDropzone({
  purpose: 'attachment',
  onFiles: (files) => console.log('picked', files),
})

Pure helpers

The matching logic is exported for reuse and testing:

import { fileMatchesAccept, matchesAcceptToken, selectFiles } from '@gentleduck/upload/react'

selectFiles(files, { accept: 'image/*', multiple: false }) // filtered, first-only
fileMatchesAccept(file, 'image/*,.pdf')                     // boolean

Next