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.
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.
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
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).
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)})
})
}
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
.
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
.
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.
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!