How to Create File Dropzone in React and TypeScript
Tags: javascript, webdev, react, tutorial
Published: 2021-10-25 06:04:26

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.

```tsx
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.

```tsx
// 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`.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
// 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.

```tsx
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.

```tsx
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.

```jsx
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.

[codesandbox]: https://codesandbox.io/s/react-file-dropzone-odphc
[create-react-app]: https://create-react-app.dev/
[classnames]: https://www.npmjs.com/package/classnames
[for loop]: https://blog.alexdevero.com/javascript-loops/#for-loop
[usestate hook]: https://blog.alexdevero.com/react-usestate-hook-in-action/
[useeffect hook]: https://blog.alexdevero.com/react-useeffect-hook/
[memoize]: https://blog.alexdevero.com/memoization-in-react/
[memo]: https://reactjs.org/docs/react-api.html#reactmemo