Upload images to your Rails API from React the easy way
23 Sep 2019 - John
I’ve been working on my React+Rails skills, but a few days ago, I noticed something that bothered me. So far I managed to build a few small applications that render images, but all of those images were already on the server as part of the application assets. What if I want to build an Image board or an Instagram clone?
I decided to take a look at it and the solution, as always, turned out to be a pretty simple one. It involves the use of ActiveStorage, which is a built-in solution introduced by rails 5.2 that offers a simplified way of handling file uploads to either your server or cloud services like Azure or S3. It’s super easy to use, so let’s dive in!
How do I get started?
After creating you new app with
rails new your-project --api --database=postgresql
all you need to do is run
rails active_storage:install
Under the hood
This will create a new migration file in your ./db/migrate
folder. If you examine this file, you’ll see it creates two tables:
- active_storage_blobs :This table is used by rails to keep track of file upload metadata. Information about the file like size, name and the such is stored in this table
- active_storage_attachments: This table is a polymorphic join table that’s used to keep the relationship between the uploads and the models they belong to.
Setting up the controller
This is nothing we haven’t done before, just a plain old controller with an index
and create
actions:
class PostsController < ApplicationController
def index
@posts = Post.all
render json: @posts
end
def create
@Post = Post.create(post_params)
end
private
def post_params
params.permit(:title, :body, :featured_image)
end
end
Of course, the actions have their respective entries in the ./config/routes.rb
file:
post "/posts", to: 'posts#create'
get "/posts", to: "posts#index"
Configuring the storage and adding the association
By default, the storage is set to use your local disk, but if you want to use a different service, you can do so by editing the self explaining ./config/storage.yml
file. Next, we want to tell Rails what storage strategy we’re going to be using. This is done in a per environment basis, so that way you can, for example, use local for development and S3 for production. The way we set this is by going to the ./config/environments
folder and setting the directive config.active_storage.service
to whatever you set your strategy in the storage.yml
file. I am currently on my development environment, so mine looks like this:
config.active_storage.service = :local
Of course, you can call the strategies whatever you want, just remember to update the changes in your desired environment. Now we need to create our models and run your migrations. For this tutorial, I’m creating a Post
model:
rails g model Post title:string body:text
After the model is created, we just need to tell it to have an image attached, so go to ./app/models/post.rb
and add the association:
has_one_attached :featured_image
This is a new type of association made available by Activestorage. You can call your image attachment whatever you want. Now we need to add the serializer gem, so open your Gemfile
, add
gem 'active_model_serializers'
and then run bundle install
. After Bundler is finished, and still in the console, run
rails g serializer post
This will create a new file in .app/serializers/post_serializer.rb
with the following contents:
class PostSerializer < ActiveModel::Serializer
attributes :id
end
We need to update this file so it looks like this:
class PostSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :title, :body, :featured_image
def featured_image
if object.featured_image.attached?
{
url: rails_blob_url(object.featured_image)
}
end
end
end
- The
include
line bring Rails URL helpers, which enables us to generate URL’s outside of the controller. What URL will we generate? The image URL, of course. - Then we’re defining the rest of our model attributes. It comes with only the id by default, so we need to fill in the missing ones. Note how we’re adding
:feature_image
to our serializer. This is possible thanks to thehas_one_attached :featured_image
association we set up in thePost
model. - We said we want the image, but we only need the URL and nothing else, so we define the
featured_image
object to only include its URL.
With this, our backend is ready. Let’s move on to the frontend!
Uploading with React
To upload images from React, we need three pieces:
- First we need to define our state like we always have
state = {
title: '',
body: '',
featured_image: null
}
We are going to handle any regular fields, in this example title
and body
, as we always have:
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
}
But the image field we will handle separately. We just set up a regular form with a file
input:
<input type="file" accept="image/*" multiple={false} onChange={this.onImageChange} />
and then we define the handler for the image field:
onImageChange = event => {
this.setState({ featured_image: event.target.files[0] });
};
The reason we do it separately is that when we send all the other field to their handlers, we can access their content through event.target.value
, but in the case of file uploads, their contents resides in an array called files
and the actual file is in the 0 index.
Now all we have to do is perform a POST
request to our API endpoint. Since we’re working with files, we can’t use json.stringify()
in our request body, so we need to send it as form data by first creating an empty formData object and then appending our state our state information. Finally, we send the formData
object as the body of our request:
handleSubmit = event => {
event.preventDefault();
const formData = new FormData();
formData.append('title', this.state.title);
formData.append('body', this.state.body);
formData.append('featured_image', this.state.featured_image);
fetch('http://localhost:3000/posts', {
method: 'POST',
body: formData
})
.catch(error=>console.log(error));
And that’s it. To check that it worked, go to the storage
folder on your Rails app. There should be a new folder, which contains your image with a new name and no file extension. This is normal, so don’t worry.
Requesting the images from React
This is even easier. If you already made a post, you can send a GET
request to http://localhost:3000/posts
. If everything went well, you should get something like this:
{
"id": 1,
"title": "I had a burger",
"body": "And it was awesome!",
"featured_image": {
"url": "http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--5fe3364316564ccb4dea4a701d248177629b6f74/burger.jpg"
}
Great, this is all we need! Now you can use the url
property of thefeatured_image
in your img
tags, for example:
<img src={props.post.featured_image.url}>
And you will get your image rendered, now go and make some awesome image app!