Uploading Images in React

sjamescarter

Stephen Carter

Posted on September 30, 2023

Uploading Images in React

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, 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.

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

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.

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

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.

const data = new FormData();
Object.keys(form).map(key => data.append(key, form[key]));
Enter fullscreen mode Exit fullscreen mode

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

class Profile < ApplicationRecord
  belongs_to :user
  belongs_to :venue
  has_one_attached :avatar
end
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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 Rirri on Unsplash

💖 💪 🙅 🚩
sjamescarter
Stephen Carter

Posted on September 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Uploading Images in React
tutorial Uploading Images in React

September 30, 2023