DEV Community

Cover image for How to Create File Dropzone in React and TypeScript
Alex Devero
Alex Devero

Posted on • Originally published at blog.alexdevero.com

How to Create File Dropzone in React and TypeScript

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'
Enter fullscreen mode Exit fullscreen mode

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]
)
Enter fullscreen mode Exit fullscreen mode

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]
)
Enter fullscreen mode Exit fullscreen mode

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]
)
Enter fullscreen mode Exit fullscreen mode

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]
)
Enter fullscreen mode Exit fullscreen mode

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)
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
rkallan profile image
RRKallan

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

(props: React.PropsWithChildren<FileListProps>) => (
Enter fullscreen mode Exit fullscreen mode

can be written as follow

(props: FileListProps) => (
Enter fullscreen mode Exit fullscreen mode

this will avoid the use of this line

App.displayName = 'App'
Enter fullscreen mode Exit fullscreen mode

The below function

    const mapFileListToArray = (files: FileList) => {
      const array = []

      for (let i = 0; i < files.length; i++) {
        array.push(files.item(i))
      }

      return array
    }
Enter fullscreen mode Exit fullscreen mode

could be written as follow

const mapFileListToArray = (files: FileList) => Array.from(files)
Enter fullscreen mode Exit fullscreen mode

My suggestion for The below code

        {files.length === 0 ? (
          <h3>No files to upload</h3>
        ) : (
          <h3>Files to upload: {files.length}</h3>
        )}
Enter fullscreen mode Exit fullscreen mode

Could be written as follow

     <h3>
        {files?.length ?  `Files to upload: ${files.length}` : "No files to upload"
    </h3>
Enter fullscreen mode Exit fullscreen mode

Also i think the use of memo and useCallback are not necessary.