Easy Image Uploads with Firebase Storage and React Dropzone (Part 2)

Easy Image Uploads with Firebase Storage and React Dropzone

This blog post is the second of a two-part series. This second part will spruce up react-dropzone and connect it to Firebase Cloud Storage.

Last time was more about setup. This time we are going to add some styling and upload functionality.

If you missed the first part of this series be sure to check it out.

Want to skip to the finished version of the code? Check the part-2 branch.

Before you start you can see the code in action.

Let's Get Started... Again

Add Charkra UI

We will use this awesome component library to very easily add some accessible an nicely styled components to our project.

yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4

Let's Code

Use Chakra Provider

In order to get Chakra UI working correctly, we need to setup the ChakraProvider in the index.js file. Replace the entire file so it looks like this:

import React from 'react'
import ReactDOM from 'react-dom'
import { ChakraProvider } from '@chakra-ui/react'

import App from './App'
import * as serviceWorker from './serviceWorker'

import './index.css'

ReactDOM.render(
  <React.StrictMode>
    <ChakraProvider>
      <App />
    </ChakraProvider>
  </React.StrictMode>,
  document.getElementById('root')
)

serviceWorker.unregister()

This wraps the entire project with some Chakra UI goodness.

Use Chakra Components

Previously, we passed a style prop to the container component located in App.js. Instead of using inline styles we can use a Chakra UI Flex and Text components.

First import the required components at the top of the file:

import { Flex, Text } from '@chakra-ui/react'

The App component will now look like this:

function App() {
  const onDrop = useCallback((acceptedFiles) => {
    // Do something with the files
  }, [])
  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })

  return (
    <Flex justify="center" align="center" textAlign="center" bg="#dadada" w={250} h={250} p={50} m={2} borderRadius={5} {...getRootProps()}>
      <input {...getInputProps()} />
      {isDragActive ? <Text>Drop the files here...</Text> : <Text>Drag 'n' drop some files here, or click to select files</Text>}
    </Flex>
  )
}

Here we are using the Flex component. The UI looks very similar, but our code is utilizing style props instead of inline styles.

We still deconstruct getRootProps() and pass in the resulting props.

There is no need to replace the input element with a Chakra UI component. That component has display: none; styling as a result of getInputProps(). Without deviating from the purpose of this blog post, we can just say this is how react-dropzone handles the input element.

You can also delete the extraneous containerStyle declaration.

Use Firebase Storage

Create a storage.js file in the src directory of the project. This file will contain all the Firebase Storage related code.

Place the following code in storage.js:

import firebase from './firebase'

export const uploadFromBlobAsync = async ({ blobUrl, name }) => {
  if (!blobUrl || !name) return null

  try {
    const blob = await fetch(blobUrl).then((r) => r.blob())
    const snapshot = await firebase.storage().ref().child(name).put(blob)
    return await snapshot.ref.getDownloadURL()
  } catch (error) {
    throw error
  }
}

We will call uploadFromBlobAsync when a file is dropped onto the dropzone. We do this by adding the following code to the onDrop function found in App.js:

const onDrop = useCallback(async (acceptedFiles) => {
  const file = acceptedFiles?.[0]

  if (!file) {
    return
  }

  try {
    await uploadFromBlobAsync({
      blobUrl: URL.createObjectURL(file),
      name: `${file.name}_${Date.now()}`,
    })
  } catch (e) {
    console.log(e)
  }
}, [])

First, we validate that we are receiving a file. If we have a found a valid file then we call uploadFromBlobAsync. Notice that we are using URL.createObjectURL to create a URL pointing to our file. That is a Web API that is compatible with most modern browsers.

Inform the User

We still need to let the user know the status of the upload. We do this by implementing React's useState hook.

First, in App.js we import useState using the existing import declaration:

import React, { useCallback, useState } from 'react'

Then at the start of the App function we use the hook to help us store state regarding the upload progress:

function App() {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [message, setMessage] = useState(null)

The three state variables will be populated from the onDrop function. We add them and the function will now look like this:

const onDrop = useCallback(async (acceptedFiles) => {
  const file = acceptedFiles?.[0]

  if (!file) {
    return
  }

  setIsLoading(true)
  setError(null)
  setMessage(null)

  try {
    await uploadFromBlobAsync({
      blobUrl: URL.createObjectURL(file),
      name: `${file.name}_${Date.now()}`,
    })
  } catch (e) {
    setIsLoading(false)
    setError(e)
    return
  }

  setIsLoading(false)
  setMessage('File was uploaded 👍')
}, [])

The function now updates the state, however we are not showing the user the active state. We can update the returning component to let the user know the upload progress.

Import the Spinner component:

import { Flex, Text, Spinner } from '@chakra-ui/react'

Then add it to the returned component:

return (
  <Flex bg="#dadada" w={250} h={250} justify="center" align="center" p={50} m={2} borderRadius={5} textAlign="center" {...getRootProps()}>
    <input {...getInputProps()} />
    {isLoading ? <Spinner /> : isDragActive ? <Text>Drop the files here...</Text> : <Text>Drag 'n' drop some files here, or click to select files</Text>}
  </Flex>
)

The Spinner component will be displayed while isLoading is true.

We can add a Chakra UI Alert component to inform the user of errors and successful uploads. Further, we only need to display it if we have a message to show.

{
  ;(error || message) && (
    <Alert status={error ? 'error' : 'success'} w={250} borderRadius={5} m={2}>
      <AlertIcon />
      <AlertDescription w={200}>{error || message}</AlertDescription>
    </Alert>
  )
}

This component will be placed after the Flex component. However, we need to wrap both in a fragment.

When all is said and done App.js will look like this:s

import React, { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone'
import { Flex, Text, Spinner, Alert, AlertIcon, AlertDescription } from '@chakra-ui/react'

import { uploadFromBlobAsync } from './storage'

function App() {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [message, setMessage] = useState(null)
  const onDrop = useCallback(async (acceptedFiles) => {
    const file = acceptedFiles?.[0]

    if (!file) {
      return
    }

    setIsLoading(true)
    setError(null)
    setMessage(null)

    try {
      await uploadFromBlobAsync({
        blobUrl: URL.createObjectURL(file),
        name: `${file.name}_${Date.now()}`,
      })
    } catch (e) {
      setIsLoading(false)
      setError(e.message)
      return
    }

    setIsLoading(false)
    setMessage('File was uploaded 👍')
  }, [])

  const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })

  return (
    <>
      <Flex bg="#dadada" w={250} h={250} justify="center" align="center" p={50} m={2} borderRadius={5} textAlign="center" {...getRootProps()}>
        <input {...getInputProps()} />
        {isLoading ? <Spinner /> : isDragActive ? <Text>Drop the files here...</Text> : <Text>Drag 'n' drop some files here, or click to select files</Text>}
      </Flex>
      {(error || message) && (
        <Alert status={error ? 'error' : 'success'} w={250} borderRadius={5} m={2}>
          <AlertIcon />
          <AlertDescription w={200}>{error || message}</AlertDescription>
        </Alert>
      )}
    </>
  )
}

export default App

Wrap Up

Go ahead and drag and drop a file. You will briefly see the loading indicator then a message will appear to inform you the file was uploaded. In the Firebase Console you can also see the file in your storage bucket. Success!

firebase uploaded

We have come a long way, now go ahead and implement this into your project. However, every project is unique so feel free to reach out to me if you hit any road blocks.