useDragAndDrop
The useDragAndDrop hook provides a complete drag and drop implementation for file uploads, including visual feedback during drag operations, support for both File objects and data URLs, and proper event handling with automatic cleanup.
Type Signature
interface UseDragAndDropProps {
onDrop ?: ( file : File ) => void
onDropDataUrl ?: ( dataUrl : string ) => void
enabled ?: boolean
}
function useDragAndDrop ( props ?: UseDragAndDropProps ) : {
isDragging : boolean
dragDropProps : { ref : RefObject < HTMLDivElement > }
dropTargetRef : RefObject < HTMLDivElement >
}
Parameters
Callback function executed when a file is dropped, receiving the File object
onDropDataUrl
(dataUrl: string) => void
Callback function executed when a file is dropped, receiving the file as a base64 data URL string
Whether drag and drop functionality is enabled. When false, drag events are ignored
Return Value
Current drag state - true when a file is being dragged over the drop zone
dragDropProps
{ ref: RefObject<HTMLDivElement> }
Props object containing the ref to spread on your drop target element
dropTargetRef
RefObject<HTMLDivElement>
Direct access to the drop target ref (same as dragDropProps.ref)
Usage Examples
Basic File Upload
import { useDragAndDrop } from '@hooks/useDragAndDrop'
import { useState } from 'react'
const FileUploader = () => {
const [ uploadedFile , setUploadedFile ] = useState < File | null >( null )
const { isDragging , dropTargetRef } = useDragAndDrop ({
onDrop : ( file ) => {
console . log ( 'File dropped:' , file . name )
setUploadedFile ( file )
},
})
return (
< div
ref = { dropTargetRef }
className = { `upload-zone ${ isDragging ? 'dragging' : '' } ` }
>
{ isDragging ? (
< p > Drop your file here </ p >
) : (
< p > Drag and drop a file here </ p >
)}
{ uploadedFile && < p > Uploaded : { uploadedFile . name }</ p >}
</ div >
)
}
Image Upload with Preview
const ImageUploader = () => {
const [ imageUrl , setImageUrl ] = useState < string >( '' )
const { isDragging , dropTargetRef } = useDragAndDrop ({
onDropDataUrl : ( dataUrl ) => {
setImageUrl ( dataUrl )
},
})
return (
< div
ref = { dropTargetRef }
className = { `image-upload ${ isDragging ? 'highlight' : '' } ` }
>
{ imageUrl ? (
< img src = { imageUrl } alt = "Preview" className = "preview" />
) : (
< div className = "placeholder" >
{ isDragging ? 'Drop image here' : 'Drag image to upload' }
</ div >
)}
</ div >
)
}
Profile Avatar Upload
const AvatarUpload = () => {
const [ avatar , setAvatar ] = useState < string >( '' )
const { isDragging , dropTargetRef } = useDragAndDrop ({
onDrop : async ( file ) => {
// Validate file type
if ( ! file . type . startsWith ( 'image/' )) {
toast . error ( 'Please upload an image file' )
return
}
// Validate file size (max 5MB)
if ( file . size > 5 * 1024 * 1024 ) {
toast . error ( 'Image must be less than 5MB' )
return
}
// Upload to server
const formData = new FormData ()
formData . append ( 'avatar' , file )
const response = await fetch ( '/api/uploadImage' , {
method: 'POST' ,
body: formData ,
})
const data = await response . json ()
setAvatar ( data . url )
toast . success ( 'Avatar uploaded successfully' )
},
})
return (
< div className = "avatar-upload" >
< div
ref = { dropTargetRef }
className = { `drop-zone ${ isDragging ? 'active' : '' } ` }
>
{ avatar ? (
< img src = { avatar } alt = "Avatar" className = "avatar" />
) : (
< div className = "placeholder" >
< UserIcon />
< p > Drop image to upload avatar </ p >
</ div >
)}
</ div >
</ div >
)
}
Both File and Data URL
const DualHandlerUpload = () => {
const [ file , setFile ] = useState < File | null >( null )
const [ preview , setPreview ] = useState < string >( '' )
const { isDragging , dropTargetRef } = useDragAndDrop ({
onDrop : ( file ) => {
// Handle the File object
setFile ( file )
uploadToServer ( file )
},
onDropDataUrl : ( dataUrl ) => {
// Handle the data URL for preview
setPreview ( dataUrl )
},
})
return (
< div ref = { dropTargetRef } >
{ preview && < img src = { preview } alt = "Preview" /> }
{ file && < p >{ file . name } ({( file . size / 1024). toFixed (2)} KB )</ p >}
</ div >
)
}
Conditional Enable/Disable
const ConditionalUpload = ({ userCanUpload } : { userCanUpload : boolean }) => {
const { isDragging , dropTargetRef } = useDragAndDrop ({
enabled: userCanUpload ,
onDrop : ( file ) => {
handleUpload ( file )
},
})
return (
< div
ref = { dropTargetRef }
className = {!userCanUpload ? 'disabled' : '' }
>
{ userCanUpload ? (
isDragging ? 'Drop file' : 'Drag file here'
) : (
'Upload disabled - please log in'
)}
</ div >
)
}
With Loading State
const UploadWithProgress = () => {
const [ uploading , setUploading ] = useState ( false )
const { isDragging , dropTargetRef } = useDragAndDrop ({
enabled: ! uploading ,
onDrop : async ( file ) => {
setUploading ( true )
try {
await uploadFile ( file )
toast . success ( 'Upload complete' )
} catch ( error ) {
toast . error ( 'Upload failed' )
} finally {
setUploading ( false )
}
},
})
return (
< div ref = { dropTargetRef } >
{ uploading ? (
< Spinner />
) : isDragging ? (
< p > Drop to upload </ p >
) : (
< p > Drag file here </ p >
)}
</ div >
)
}
Styling the Drop Zone
CSS Example
.drop-zone {
border : 2 px dashed #ccc ;
border-radius : 8 px ;
padding : 40 px ;
text-align : center ;
transition : all 0.3 s ease ;
background-color : #fafafa ;
}
.drop-zone.dragging {
border-color : #4f46e5 ;
background-color : #eef2ff ;
transform : scale ( 1.02 );
}
.drop-zone.disabled {
opacity : 0.5 ;
cursor : not-allowed ;
background-color : #f5f5f5 ;
}
Tailwind CSS Example
const TailwindDropZone = () => {
const { isDragging , dropTargetRef } = useDragAndDrop ({
onDrop: handleDrop ,
})
return (
< div
ref = { dropTargetRef }
className = { `
border-2 border-dashed rounded-lg p-8 text-center
transition-all duration-300
${ isDragging
? 'border-blue-500 bg-blue-50 scale-105'
: 'border-gray-300 bg-gray-50'
}
hover:border-blue-400
` }
>
< UploadIcon className = "mx-auto mb-4" />
< p >{isDragging ? 'Drop it!' : 'Drag files here' } </ p >
</ div >
)
}
Use Cases
Profile avatar uploads with image preview
Document uploads for anime reviews or comments
Batch image uploads for galleries
File attachments in messaging systems
Cover image uploads for user-created lists
Screenshot sharing for anime discussions
Banner image uploads for custom profiles
File Validation
Type Validation
Size Validation
Combined Validation
const { isDragging , dropTargetRef } = useDragAndDrop ({
onDrop : ( file ) => {
const allowedTypes = [ 'image/jpeg' , 'image/png' , 'image/webp' ]
if ( ! allowedTypes . includes ( file . type )) {
toast . error ( 'Only JPEG, PNG, and WebP images are allowed' )
return
}
uploadFile ( file )
},
})
How It Works
Drag Counter : Uses a counter to handle nested drag enter/leave events correctly
Event Prevention : Prevents default browser behavior for drag events
Visual Feedback : Provides isDragging state for UI updates
File Processing : Extracts the first file from the drop event
Dual Callbacks : Supports both File object and data URL callbacks
Cleanup : Automatically removes event listeners on unmount
The hook only processes the first file if multiple files are dropped. To handle multiple files, you’ll need to modify the implementation or handle it in your onDrop callback.
Accessibility
Always provide an alternative file input for users who can’t or prefer not to use drag and drop: < div ref = { dropTargetRef } >
< p > Drag and drop or </ p >
< input
type = "file"
onChange = {(e) => handleFile (e.target.files?.[ 0 ])}
aria - label = "Upload file"
/>
</ div >
Browser Compatibility
The drag and drop API is supported in all modern browsers:
✅ Chrome/Edge (all versions)
✅ Firefox (all versions)
✅ Safari (all versions)
✅ Opera (all versions)
Mobile browsers have limited drag and drop support. Consider providing a traditional file input as a fallback for mobile users.
Source
Location: src/domains/shared/hooks/useDragAndDrop.ts:47