DEV Community

Cover image for Uploading Profile Pictures in a React and Rails API Application Part I
Rachel Williams
Rachel Williams

Posted on

Uploading Profile Pictures in a React and Rails API Application Part I

The Problem

This week I wanted to add a feature to my Concert Buddy application where users could add an image to their profile. It turns out that this is not as easy as I originally thought. I had to decide whether to store the images in my database or on a cloud service.

Storing in the Database

After some research, I found that there is a way to store images in a database and that is by using BLOBS. BLOBS are Binary Large Objects meaning the image is stored as, "a collection of binary data stored as a single entity." However, it seems like the general consensus among developers is to not store image data in your database because it takes up a large amount of storage. From this Stack Overflow post:

"database storage is usually more expensive than file system storage" and "things like web servers, etc, need no special coding or processing to access images in the file system"

Also, I used PostgreSQL for the database in my application which uses bytea instead of the BLOB type. Uploading images to a cloud service seemed like a better plan for my profile picture images. This way, my database wouldn't get too large and I could also learn how to work with a cloud service.

The Implementation

I ended up finding this amazing blog that walks through how to send images from a JavaScript front end to a Rails API backend. It was exactly what I needed! I will walk through the basic implementation here and talk about the hiccups I had, but the blog already has great information on how to do it yourself.

The First Step

The first step to starting this feature was to add an input for a user to select a file from their computer. I added a form to my Profile component with one input for the image, as well as a submit button:

<form className={props.formStatus === "Active" ? "" : "hidden"} onSubmit={submitPhoto}>
  <input type="file"
    id="profile-photo-input" name="profile_picture"
    accept="image/png, image/jpeg"
  />
  <input
    className="submit-input"
    type="submit"
    value="Upload"
  />
</form>


By using the file type input, a user can select a file from their computer to upload. File type inputs allow an accept attribute to specify which types of files are allowed. You can also choose to use the multiple attribute which allows users to upload multiple files at once. In my case, I only want users to upload one image so I left the multiple attribute out and specified image/png and image/jpeg with the accept attribute to allow those file types.

Sending the Photo

The next step in the process was sending the photo via a fetch request to my Rails API.

From the code snippet above, you can see that my form has an onSubmit event attribute. This will call the submitPhoto function in my component when the form is submitted. Here is my code for that function:

const submitPhoto = (event) => {
    event.preventDefault();
    let photoInput = document.getElementById('profile-photo-input');
    if (photoInput.files[0]) {
      const formData = new FormData();
      const upload_file = photoInput.files[0]
      formData.append('profile_picture', upload_file);
      props.submitProfilePhoto(formData, props.userId)
    }
  }

The main thing to focus on here is setting up the FormData object. I had never used this before, however it was used in the main blog that I followed. This object allows you to set key/value pairs for all of the data you are submitting in your form and your subsequent fetch request. In this case, I chose to name my key 'profile_picture' and the value is the uploaded image. I grabbed the image by grabbing the file input from the DOM and then used the file input's files method to get the FileList. If the image was successfully selected, there should be a file in the first index of FileList.

Next up was to make the action creator function to perform the fetch request. I called this function submitProfilePhoto when I declared it in mapDispatchToProps. The actual function is called uploadProfilePicture and that is what was imported into the file with my component. Here is the code for that:

const mapDispatchToProps = dispatch => {
  return {
    toggleForm: () => dispatch(setFormStateToActive()),
    submitProfilePhoto: (formData, userId) => dispatch(uploadProfilePicture(formData, userId))
  }
}

Above you can see I passed the formData and userId to the dispatch method wrapping the uploadProfilePicture function. In case you aren't familiar with dispatch, it is a store method in the Redux library that allows you to send actions to your store and trigger state changes. Using it along with Thunk middleware is very useful for asynchronous requests because the action creators can then return functions that can have side effects, such as asynchronous requests. Then after your request is complete, you can trigger a state change. As a side note, this code could definitely use some refactoring in the future.

Here is my Thunk action creator function:

export const uploadProfilePicture = (formData, userId) => {
  return dispatch => {
    const configurationObject = {
      credentials: "include",
      method: "POST",
      body: formData
    }

    return fetch(`${baseUrl}/api/v1/users/${userId}/upload_photo`, configurationObject)
      .then(r => r.json())
      .then(photo => {
        if (photo.error) {
          alert(photo.error)
        } else {
          // this is where I will dispatch an action creator function to update my store
          console.log("success", photo)
        }
      })
      .catch(error => console.log(error))
  }
}

Above you can see I set up the configurationObject, setting the body to the formData object I created earlier. This is the code I had success with after some trial and error.

A Bug

At first I had specified a Content-Type in my configurationObject and was getting a bad request response (status 400) when sending my FormData to Rails.

After some searching I found this:

"Setting the Content-Type header manually means it's missing the boundary parameter. Remove that header and allow fetch to generate the full content type. It will look something like this:

Content-Type: multipart/form-data;boundary=----WebKitFormBoundaryyrV7KO0BoCBuDbTL

Fetch knows which content type header to create based on the FormData object passed in as the request body content."

Also, if I would have looked more closely in the blog I was following, I would have noticed that it talks about this in there as well.

"There is no "Content-Type" key in the headers — the content type is multipart/form-data, which is implied by the FormData object itself."

I removed the content-type from my request and it worked! Also, I should mention that I had already set up the route and controller action in my Rails API along with a byebug to test that the data hit the endpoint. I will go over this further in my next blog.

For the Next Blog

As this blog is already getting pretty long, I decided that I'm going to split it into multiple parts.

Next week I will talk about the Rails side of things and how I am hosting my images on a cloud service, Cloudinary.

Thank you for reading!

Additional Resources

400 Status Code
Main Blog I Followed
Another Blog I Referenced
File Input

Top comments (0)