There are various solutions for file dropzones. Some are simple, some complicated. This tutorial will help you create your own simple file dropzone. You will learn how to handle various drag and drop events, how to process dropped files and how to create a simple API for reusable dropzone component.
Demo on Codesandbox.
A brief introduction
In this tutorial we will create a simple file dropzone from scratch without any special dependencies. We will create this app using the create-react-app, with the TypeScript template (--template typescript
flag). This will give us all resources we need, almost.
Along with the default React and TypeScript dependencies, we will also add classnames library. We will use this library for appending class to the file dropzone when it is active. This means when someone drags a file over it. This class will apply some CSS styles to highlight the dropzone.
Using the create-react-app
template will generate some stuff we can remove. This includes the logo and the content of App.tsx
. However, you can leave the content of the App component as it is for now. We will replace it later with the file dropzone and list of files. Now, let's take a look at the dropzone.
Creating Dropzone component
The idea of custom file dropzone component may look complicated. However, this is not necessarily the truth. The logic for dropzone will require us to handle few drag and drop events, some simple state management for active state and processing of dropped files. That's basically it.
For state management, we will use the React useState hook. Next, we will also use useEffect hook for attaching event listeners and observing dropzone's state. Lastly, we will also memoize every component using memo HOC. Let's start building.
Getting started
The first thing we need is to define the file dropzone component. This also includes defining some interface for its props
, or component API. The dropzone component will accept six event handlers. Four of these handlers will be invoked on events such as dragenter
, dragleave
, dragover
and drop
.
These handlers will allow anyone using this dropzone component execute some code when these events fire. The fifth and sixth handler will be synthetic. One will be invoked when the state of the dropzone active changes. This means when someone is dragging a file over it and when the drag is over.
Any time this happens the handler for this will be invoked, it will pass boolean value specifying current active/non-active state. The sixth event will be invoked when files are dropped on the dropzone. This handler will pass files dropped on the dropzone so they can be processed elsewhere in the app.
The dropzone itself will be a <div>
element with ref
. We will use this ref
to attach event listeners to the dropzone when the component mounts, and to remove them when it unmounts. To make this dropzone more usable, we will set it up so it renders children passed through props.
This means that we will be able to use this dropzone as a wrapper for other content, without removing the content itself.
import React from 'react'
// Define interface for component props/api:
export interface DropZoneProps {
onDragStateChange?: (isDragActive: boolean) => void
onDrag?: () => void
onDragIn?: () => void
onDragOut?: () => void
onDrop?: () => void
onFilesDrop?: (files: File[]) => void
}
export const DropZone = React.memo(
(props: React.PropsWithChildren<DropZoneProps>) => {
const {
onDragStateChange,
onFilesDrop,
onDrag,
onDragIn,
onDragOut,
onDrop,
} = props
// Create state to keep track when dropzone is active/non-active:
const [isDragActive, setIsDragActive] = React.useState(false)
// Prepare ref for dropzone element:
const dropZoneRef = React.useRef<null | HTMLDivElement>(null)
// Render <div> with ref and children:
return <div ref={dropZoneRef}>{props.children}</div>
}
)
DropZone.displayName = 'DropZone'
DragEnter event
First event we will deal with is the dragenter
event. This event will be triggered when a file enters the dropzone, someone takes a file and places it over the drop zone. We will use this event to do two things. First, we will invoke any optional method passed as onDragIn()
through props.
Second, we will check if someone is really dragging a file over the dropzone. If so, we will set the active state of the dropzone to true
. We will also prevent any default events and propagation. That's all we need for this event.
// Create handler for dragenter event:
const handleDragIn = React.useCallback(
(event) => {
// Prevent default events:
event.preventDefault()
event.stopPropagation()
// Invoke any optional method passed as "onDragIn()":
onDragIn?.()
// Check if there are files dragging over the dropzone:
if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
// If so, set active state to "true":
setIsDragActive(true)
}
},
[onDragIn]
)
DragLeave event
Handling the dragleave
event will be also very easy. This event will be fired when some file left the dropzone, when it is no longer hovering over it. To handle this event we have to do few things. First, we will again prevent any default events and propagation.
The second thing to do is to invoke any optional method passed as onDragOut()
through props. After that, we will also need to set the active state to false
.
// Create handler for dragleave event:
const handleDragOut = React.useCallback(
(event) => {
// Prevent default events:
event.preventDefault()
event.stopPropagation()
// Invoke any optional method passed as "onDragOut()":
onDragOut?.()
// Set active state to "false":
setIsDragActive(false)
},
[onDragOut]
)
Drag event
Handler for dragover
event will help us ensure the dropzone active state is true
when something is being dragged over it. However, we will not simply set the active state to true
. Instead, we will first check if the current state value is false
and only then change it to true
.
This will help us avoid some state changes that are not necessary. We will also use this event to invoke any method passed as onDrag()
through the props.
// Create handler for dragover event:
const handleDrag = React.useCallback(
(event) => {
// Prevent default events:
event.preventDefault()
event.stopPropagation()
// Invoke any optional method passed as "onDrag()":
onDrag?.()
// Set active state to "true" if it is not active:
if (!isDragActive) {
setIsDragActive(true)
}
},
[isDragActive, onDrag]
)
Drop event
The drop
event is the most important event we need to take care of. Its handler will also be the longest. This handler will do a couple of things. First, it will prevent any default behavior and stop propagation. Next, it will set the dropzone active state to false
.
This makes sense because when something is dropped to the area, the drag event is over. Dropzone should register this. When the drop event is fired, we can also invoke any optional method passed as onDrop()
through props. The most important part is are those dropped files.
Before we take care of them, we will first check if there are any files. We can do this by checking the event.dataTransfer.files
object and its length
property. If there are some files, we will invoke any method passed as onFilesDrop()
through the props.
This will allow us to process those files as we want outside the dropzone. When we dispatch those files, we can clear the dataTransfer
data to prepare the dropzone for another use. There is one important thing about the files. We will get these files in the form of FileList
not an array.
We can easily convert this FileList
to an array using for loop. This loop will go through the files in dataTransfer
object and push each into an empty array. We can then pass this array as an argument into any method onFilesDrop()
to get the files where they are needed.
// Create handler for drop event:
const handleDrop = React.useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
// Prevent default events:
// Set active state to false:
setIsDragActive(false)
// Invoke any optional method passed as "onDrop()":
onDrop?.()
// If there are any files dropped:
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
// Convert these files to an array:
const filesToUpload = []
for (let i = 0; i < event.dataTransfer.files.length; i++) {
filesToUpload.push(event.dataTransfer.files.item(i))
}
// Invoke any optional method passed as "onFilesDrop()", passing array of files as an argument:
onFilesDrop?.(filesToUpload)
// Clear transfer data to prepare dropzone for another use:
event.dataTransfer.clearData()
}
},
[onDrop, onFilesDrop]
)
Effects
Handlers are done and ready. Before we can move on we need to set up two useEffect
hooks. One hook will be for observing the active state. When this state changes we want to invoke any method passed as onDragStateChange()
through props, passing current state value as an argument.
The second effect will attach all handlers we just created to the dropzone <div>
element when it mounts. After this, the dropzone will be ready to use. We will also use this effect to remove all event listeners when the dropzone unmounts. We will do this through the clean up method.
// Obser active state and emit changes:
React.useEffect(() => {
onDragStateChange?.(isDragActive)
}, [isDragActive])
// Attach listeners to dropzone on mount:
React.useEffect(() => {
const tempZoneRef = dropZoneRef?.current
if (tempZoneRef) {
tempZoneRef.addEventListener('dragenter', handleDragIn)
tempZoneRef.addEventListener('dragleave', handleDragOut)
tempZoneRef.addEventListener('dragover', handleDrag)
tempZoneRef.addEventListener('drop', handleDrop)
}
// Remove listeners from dropzone on unmount:
return () => {
tempZoneRef?.removeEventListener('dragenter', handleDragIn)
tempZoneRef?.removeEventListener('dragleave', handleDragOut)
tempZoneRef?.removeEventListener('dragover', handleDrag)
tempZoneRef?.removeEventListener('drop', handleDrop)
}
}, [])
Putting it together
These are all the parts we need for the File dropzone component. When we put all these parts together we will be able to use this component anywhere in the React app.
import React from 'react'
// Define interface for component props/api:
export interface DropZoneProps {
onDragStateChange?: (isDragActive: boolean) => void
onDrag?: () => void
onDragIn?: () => void
onDragOut?: () => void
onDrop?: () => void
onFilesDrop?: (files: File[]) => void
}
export const DropZone = React.memo(
(props: React.PropsWithChildren<DropZoneProps>) => {
const {
onDragStateChange,
onFilesDrop,
onDrag,
onDragIn,
onDragOut,
onDrop,
} = props
// Create state to keep track when dropzone is active/non-active:
const [isDragActive, setIsDragActive] = React.useState(false)
// Prepare ref for dropzone element:
const dropZoneRef = React.useRef<null | HTMLDivElement>(null)
// Create helper method to map file list to array of files:
const mapFileListToArray = (files: FileList) => {
const array = []
for (let i = 0; i < files.length; i++) {
array.push(files.item(i))
}
return array
}
// Create handler for dragenter event:
const handleDragIn = React.useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
onDragIn?.()
if (event.dataTransfer.items && event.dataTransfer.items.length > 0) {
setIsDragActive(true)
}
},
[onDragIn]
)
// Create handler for dragleave event:
const handleDragOut = React.useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
onDragOut?.()
setIsDragActive(false)
},
[onDragOut]
)
// Create handler for dragover event:
const handleDrag = React.useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
onDrag?.()
if (!isDragActive) {
setIsDragActive(true)
}
},
[isDragActive, onDrag]
)
// Create handler for drop event:
const handleDrop = React.useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
setIsDragActive(false)
onDrop?.()
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
const files = mapFileListToArray(event.dataTransfer.files)
onFilesDrop?.(files)
event.dataTransfer.clearData()
}
},
[onDrop, onFilesDrop]
)
// Obser active state and emit changes:
React.useEffect(() => {
onDragStateChange?.(isDragActive)
}, [isDragActive])
// Attach listeners to dropzone on mount:
React.useEffect(() => {
const tempZoneRef = dropZoneRef?.current
if (tempZoneRef) {
tempZoneRef.addEventListener('dragenter', handleDragIn)
tempZoneRef.addEventListener('dragleave', handleDragOut)
tempZoneRef.addEventListener('dragover', handleDrag)
tempZoneRef.addEventListener('drop', handleDrop)
}
// Remove listeners from dropzone on unmount:
return () => {
tempZoneRef?.removeEventListener('dragenter', handleDragIn)
tempZoneRef?.removeEventListener('dragleave', handleDragOut)
tempZoneRef?.removeEventListener('dragover', handleDrag)
tempZoneRef?.removeEventListener('drop', handleDrop)
}
}, [])
// Render <div> with ref and children:
return <div ref={dropZoneRef}>{props.children}</div>
}
)
DropZone.displayName = 'DropZone'
Adding simple file list component
One nice addon to the dropzone can be file list showing all files dropped to the dropzone. This can make the UI more user-friendly as users will now what files were registered by the app. This list doesn't have to be complicated. It can show just the name of the file and its size.
This file list component will be simple. It will accept an array of files through props
. It will then map over this array and generate <li>
with name and file size for each file. All list items will be wrapped with <ul>
element.
import React from 'react'
export interface FileListProps {
files: File[]
}
export const FileList = React.memo(
(props: React.PropsWithChildren<FileListProps>) => (
<ul>
{props.files.map((file: File) => (
<li key={`${file.name}_${file.lastModified}`}>
<span>{file.name}</span>{' '}
<span>({Math.round(file.size / 1000)}kb)</span>
</li>
))}
</ul>
)
)
FileList.displayName = 'FileList'
Creating the App component and making it work
The file dropzone and file list are ready. This means that we can now go to the App.tsx
and replace the default content. Inside the App
component, we will need to create two states. One will be for keeping track of dropzone active state. We will use this to highlight the dropzone when dragging is happening.
The second state will be for any files dropped into the dropzone. We will also need two handlers. One will be for the dropzone's onDragStateChange()
method. We will use this handler to update local active state. The second handler will be for dropzone's onFilesDrop()
.
We will use this handler to get any files dropped into the dropzone outside it, into local files
state. We will attach both these handlers to the Dropzone
component. For the dropzone and file list, we will put them in the render section of the App
component.
import React from 'react'
import classNames from 'classnames'
// Import dropzone and file list components:
import { DropZone } from './Dropzone'
import { FileList } from './Filelist'
export const App = React.memo(() => {
// Create "active" state for dropzone:
const [isDropActive, setIsDropActive] = React.useState(false)
// Create state for dropped files:
const [files, setFiles] = React.useState<File[]>([])
// Create handler for dropzone's onDragStateChange:
const onDragStateChange = React.useCallback((dragActive: boolean) => {
setIsDropActive(dragActive)
}, [])
// Create handler for dropzone's onFilesDrop:
const onFilesDrop = React.useCallback((files: File[]) => {
setFiles(files)
}, [])
return (
<div
className={classNames('dropZoneWrapper', {
'dropZoneActive': isDropActive,
})}
>
{/* Render the dropzone */}
<DropZone onDragStateChange={onDragStateChange} onFilesDrop={onFilesDrop}>
<h2>Drop your files here</h2>
{files.length === 0 ? (
<h3>No files to upload</h3>
) : (
<h3>Files to upload: {files.length}</h3>
)}
{/* Render the file list */}
<FileList files={files} />
</DropZone>
</div>
)
})
App.displayName = 'App'
Conclusion: How to create file dropzone in React and TypeScript
There you have it! You've just created a custom file dropzone component. Since this it is a standalone component you can take it and use it anywhere you want and need. I hope you enjoyed this tutorial. I also hope this tutorial helped you learn something new and useful.
Top comments (1)
With react html elements has also attributes onDrop, onDragOver, onDragEnter and onDragLeave.
So you can avoid the use of useRef and the useEffect to add / remove eventListener
the below code
can be written as follow
this will avoid the use of this line
The below function
could be written as follow
My suggestion for The below code
Could be written as follow
Also i think the use of memo and useCallback are not necessary.