Uploading Images in React
Tags: tutorial, react, javascript, rails
Published: 2023-09-30 14:43:11

One of the requirements for my final project at Flatiron School was to incorporate something that had not previously been taught in the coursework. Due to my love for beautiful design, I chose image uploading to complete this requirement. Thinking through the request-response cycle, I needed to learn how to select an image through the file browser, how to upload the image through a POST or PATCH request, how to store the image in the database, and how to return the image with a GET request.

## Setup
Before we go any further, my app is built with: 

- React v18.2.0
- Ruby v2.7.4
- Rails v7.0.5

## File Selection
While I actually started with the backend in my own learning process, I thought it made sense to present this tutorial in the order of the request-response cycle. 

In addition to being able to access the file browser to select an image to upload, I also wanted to be able to drag'n'drop that image as well. While researching a few options, I came across [react-dropzone](https://react-dropzone.js.org/), a "Simple React hook to create a HTML5-compliant drag'n'drop zone for files." Installation was easy, and the docs were well-written with many examples of different applications. 

Initially, I had hoped react-dropzone was an uploader as well, but upon further reading of the docs, I discovered it was not. The docs do recommend other uploaders, but as I learned soon after, uploading in React is super easy. 

I created a DropZone component to be used in various forms throughout my application. This component receives a `setState` prop used in the `useCallback` hook. While the DropZone can handle files of any type, I customized mine to accept images only.

```react
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

function DropZone({ setState }) {
  const onDrop = useCallback(acceptedFiles => {
    setState(acceptedFiles[0])
  }, []);

  const {getRootProps, getInputProps, isDragActive} = useDropzone({
    onDrop, 
    accept: {'image/*': []}
  });

  return (
    <div {...getRootProps()} className={isDragActive ? 'active' : ""}>
      <input {...getInputProps()} />
      {
        isDragActive
          ? <p>Drop the file here ...</p> 
          : <p>Drag 'n' drop an image here—or click to select file</p>
      }
    </div>
  );
}

export default DropZone;
```
This is a simple use of react-dropzone. It has many more options including the ability to preview the image after it's been loaded in state. 

With the image file selected and successfully in state, the next objective was to upload it to the server.

## Attach & Fetch
This step took a bit of research. With a normal POST or PATCH request, I would use `JSON.stringify()` in the body of the request. However, when it came to attaching the file, that solution was unsuccessful. 

Instead, I used the FormData constructor and its append() method to attach the image file. 
```react
function handleImgSubmit(e, setErrors, form, img, setUser) {
    e.preventDefault();
    setErrors();

    const profile = new FormData();
    profile.append('first_name', form.firstName); 
    profile.append('last_name', form.lastName);
    profile.append('phone', form.phone); 
    profile.append('city', form.city);
    profile.append('state', form.state);
    profile.append('bio', form.bio);
    profile.append('venue_id', form.venueId);
    profile.append('video_url', form.videoUrl);

    if(img) {
        profile.append('avatar', img);
    }

// Important! Do not add headers to fetch requests when using FormData()
    fetch("/profiles", {
        method: "POST",
        body: profile
    })
    .then(r => {
        if(r.ok) {
            r.json().then((data) => { setUser(data) });
        } else {
            r.json().then((err) => setErrors(err.errors));
        }
    });
}
```
This method works flawlessly when sending files to the backend. **Notice the fetch request does not include `headers`, or `JSON.stringify()`.** The FormData constructor takes care of both of these concerns and submitting the fetch request with `headers` will result in a server error. FormData can be used with any request whether a file is attached or not. It saves a bit of coding when refactored like this example.
```react
const data = new FormData();
Object.keys(form).map(key => data.append(key, form[key]));
```
With the frontend complete, the next step is to create a place for the file to land.

## Managing the Database
There are many different ways to manage file storage on the backend. Since my backend is a Rails API, I thought it made sense to use [Active Storage](https://edgeguides.rubyonrails.org/active_storage_overview.html). As stated in the docs, image processing transformation requires third-party software and an `image_processing` gem. Active storage allows easy configuration for online storage services offered through Amazon, Google, or Microsoft Azure. 

In order to associate an image with a record, use the `has_one_attached` macro. 
```ruby
class Profile < ApplicationRecord
  belongs_to :user
  belongs_to :venue
  has_one_attached :avatar
end
```
Rails 6.0+ also allows for a migration to include `attachment` as the attribute type. `rails generate model Profile avatar:attachment`

In the controller, call `avatar.attach` to attach the image file to the profile.
```ruby
class ProfilesController < ApplicationController
  def create
    profile = @current_user.build_profile(profile_params) if @current_user.profile.nil?
    profile.avatar.attach(profile_params[:avatar]) unless profile_params[:avatar].nil?
    venue = Venue.find(profile_params[:venue_id])
    venue.profiles << profile
    profile.save!
    render json: @current_user, status: :created
  end

  private
  def profile_params
    params.permit(:avatar, :first_name, :last_name, :bio, :phone, :city, :state, :venue_id, :video_url)
  end
end
```
Now, the image file is attached and associated with the proper record. How do we send it back to the frontend?

## The Response
Once the file has been uploaded to the server, it’s time to send it back to the frontend. What actually gets sent to the front end is not the file, but rather, a link to the file’s location on the server. Working with the link on the front end is just like working with a link to any other URL.

Rails includes a helper to access the URL for the attached image file. First `include` the helper in the class. Then create a private method using the `url_for` helper to access the image file's URL.
```ruby
class ProfileSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers

  attributes :id, :avatar, :first_name, :last_name, :phone, :city, :state, :bio, :video_url
  has_one :user
  has_one :venue

  private
  def avatar
    url_for(object.avatar) if object.avatar.attached?
  end
end
```
Now the URL for the image file will be serialized when the controller renders a JSON. 

## Conclusion
So that's it! From front to back and back to front, working with image files is relatively straightforward. Utilizing React hooks like react-dropzone makes it very simple to put together a slick file selector. The FormData constructor handles the frontend attaching, and Active Storage manages the server-side associations and access. Returning the image is now simply returning the URL for the image. Hope this blog helps. Happy coding!

###
####
#####
###### Credit
Photo by <a href="https://unsplash.com/@rirri01?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Rirri</a> on <a href="https://unsplash.com/photos/z4KhbVhPP7s?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>