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 the has_one_attached :featured_image association we set up in the Post 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!