Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save everaldo/34e2b84f381b7e4423186b8c38810f25 to your computer and use it in GitHub Desktop.
Save everaldo/34e2b84f381b7e4423186b8c38810f25 to your computer and use it in GitHub Desktop.
Upload images with React and the carrierwave Ruby gem for Rails 5

Upload images with React and the carrierwave Ruby gem for Rails 5

I recently attended a 4.5 month, intensive coding bootcamp in Boston, MA called Launch Academy. I spent a lot of time in academic science doing imaging research, so I naturally had an interest in learning how to manipulate images in this new web browser environment. For my capstone project, I created a very simple image editing Rails app where I realized I needed to be able to save/upload images to my database.

Since, through earnest effort, I wanted my app to use React for a single-page experience, and I quickly discovered that my desire to use a React/fetch/carrierwave/fog/Rails strategy was initially difficult to learn. I had to pull information from many sources (StackOverflow, Medium, official docs, other blog posts, course instructors, etc) to develop a method that uses this specific POSTing cycle.

This blog post represents my findings to integrate all these technologies in a modern, seamless way to allow one to upload images from React inside a Rails app. I will take a top down approach and start with the frontend and move to the backend.

Dependencies and versions

To setup React inside the app I used the ruby webpacker gem. But, this is obviously not necessary to be able to integrate React. I use bundler to manage my Ruby gems, but this is also, obviously, not necessary. I am pulling these gem versions and node/yarn package versions from my Gemfile.lock file and my package.json file, respectively:

gems:
rails (5.1.4)
json (2.1.0)
fog (1.42.0)
dotenv-rails (2.2.1)
carrierwave (1.2.1)

node packages:
"react": "^16.0.0"
"react-dom": "^16.0.0"
"react-dropzone": "^4.2.1"
"webpack-dev-server": "^2.9.3" (devDependency)

I also will assume a basic understanding of React and lifecycle methods in this post which are necessary for using this method. This post also assumes knowledge of ActiveRecord and PostgreSQL which are necessary for saving the data to the backend.

Frontend

I will start with the JSX inside React and describe how to do the event handling to load the image into the browser to be POSTed to the server

Get a file from local machine with Dropzone or input

First, we will need to select the file. There are two ways to do this which are both good options. One can use a third party <Dropzone> component or the <input type='file'> HTML tag. I personally prefer Dropzone since it adds slick, drag-and-drop functionality to any child element in your JSX, but <input> essentially works exactly the same way.

The JSX for <Dropzone> (the simplest implementation):

render() {
  return(
    <div>
      <Dropzone onDrop={this.readFile}>
        <button>Upload a new image</button>
      </Dropzone>
    </div>
  )
}

The JSX for <input> (with that pesky description text; also, note that the input tag uses onClick instead of onDrop for event handling):

render() {
  return(
    <div>
      <input type='file' onClick={this.readFile}>
    </div>
  )
}

These elements use event handlers to look for new files coming into the JS so that the file(s) can be passed to the instance method that you bind in the constructor and allow for the business logic necessary to upload the file(s).

Receive the file from the the rendered JSX and prepare the file to send to the backend

Next, we need to add our file to a FormData object so that it can be POSTed to the backend. In this case, we have an instance method (as in 'instance' or our component class) called readFile() that will package our file and pass the form into another instance method for POSTing.

Accept the file as an argument and append it to a form:

readFile(files) {
  // logic validation for existence of file(s);
  // we index at 0 here since the JSX could give us multiple files or single
  // file; either way, we get an array and we only need the first element
  // in the case of single file upload

  if (files && files[0]) {
    let formPayLoad = new FormData();
    formPayLoad.append('uploaded_image', files[0]);
    this.sendImageToController(formPayLoad)
  }
}

IMPORTANT NOTE: the uploaded_image key is what you will use to reference your file object inside the params at the controller.

Now, to POST the image, we will use fetch from HTML5 that will allow us to submit a multipart form to the server. The simplest way to do this is to not pass in any headers, and let fetch implicitly handle that business logic for you.

Our sendImageToController() method called from our readFile() method will perform the asynchronous fetch call like so:

sendImageToController(formPayLoad){

  fetch(`/your/api/namespace/endpoint/${withDynamicString}/${forParams}`, {
    credentials: 'same-origin',
    headers: {},
    method: 'POST',
    body: formPayLoad
  })

  // might be a good idea to put error handling here

  .then(response => response.json())
  .then(imageFromController => {
    // optionally, you can set the state of the component to contain the image
    // object itself that was returned from the rails controller, completing
    // the post cycle
    this.setState({uploads: this.state.uploads.concat(imageFromController)})
  })
}

Backend

We nicely created a React component that works totally asynchronously from the backend. fetch is now attempting to submit some data to the server at our API endpoint that we define in our routes.rb file. We will be using the RESTfull create action to save our file reference to the database (via carrierwave) and automagically upload the file to the Amazon S3 CDN via fog.

Controller code

Like any file in Rails, we must follow the convention, so the API controller will be found inside the app/controllers/your/api/namespace/endpoint/ path (which is referenced in the fetch call) with the filename uploads_controller.rb. My database table is called 'uploads', but it can be called anything. Just make sure the name of the controller corresponds to the table where you want to save your file.

# note that 'Api::V1::UploadsController' comes from how I defined my routes
class Api::V1::UploadsController < ApplicationController

  # these lines allow me to access the 'current_user' hash
  skip_before_action :verify_authenticity_token
  before_action :authenticate_user!

  def create
    newImage = Upload.new
    # note that the 'file' key for the 'newImage' hash corresponds to the field
    # in the database table where the image file reference is stored
    newImage.file = params["uploaded_image"]
    newImage.user = current_user
    if newImage.save
      render json: Upload.last
    end
  end
end

As I referenced above, the file object is stored inside a form which gets read by rails as params. Since I defined how the file would be stored inside the form in the JavaScript, I can reference that key/value pair in the controller to access the submitted file. We can then save the file with ActiveRecord and send it back to the frontend with render json: Upload.last.

Model code

We also have to make sure that when we attempted to save the image with newImage.save that ActiveRecord knows where to send the file. We mount out image uploader automatically generated with carrierwave to the Upload model code:

class Upload < ApplicationRecord
  # put table relations here

  # ActiveRecord validations
  validates :file, presence: true
  validates :user_id, presence: true

  # this allows carrierwave to read the file, save the reference to the database
  # and pass the file over to fog which uploads the image to Amazon or another
  # CDN in production
  mount_uploader :file, FileUploader
end

As long as the validations don't fail, the data will be saved pretty seamlessly to the database and to your CDN.

Final thoughts

I hope this post was useful to people trying to use this specific stack for uploading images with React and Rails. There are some really important improvements that can be made, e.g. whitelisting file types in both the frontend and backend and sanitizing the data that comes into the server, and I will update this post with those changes.

Good luck and enjoy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment