React + Rails: File Uploads using Dropzone, Paperclip, and Rails 5

React + Rails: File Uploads using Dropzone, Paperclip, and Rails 5

I’ve spent a lot of time building React components, but I haven’t had the pleasure of handling uploads. After struggling to find an elegant solution, I think this one does the trick just fine. We don’t have to modify how Rails handles things on the receiving end, which will make this easy for you if you’re retrofitting your old jQuery stuff with shiny new React components.

Pre-Requisites

This is a follow-up from my previous tutorial about getting React running on Rails. It takes about 5 minutes to run through that tutorial, so I’d highly suggest getting set up from there. It’s a toy example that just demonstrates getting a Rails-ified Webpack running and getting your components spooled into the Rails Assets pipeline.

What We’re Working With

de
If you don’t know what these guys are, just have a peek at their documentation to get a sense of how we’ll be using them in our project.

  • Superagent: We’ll build our requests with this. I like it because I prefer callbacks over promises. Alternatively you could use Axios.
  • React-Dropzone: The React-ready clicky-draggy box you’ve probably worked with before. See the OG dropzone.js here
  • Rails 5.2
  • Paperclip: My favourite upload handler for Rails. Things just work out of the box and it plays nicely with EC2.

 

Where We Left Off…

The last React & Rails tutorial was a toy example to build a time tracker that spat out a duration to a given date in plain English. We used Moment.js as the toy NPM package, and built a simple component to print the date. I’ll continue on like this is a part 2 from the last example. The first thing we’ll be doing is setting up Paperclip.

 

Set Up Paperclip

For more information: https://github.com/thoughtbot/paperclip

  1. Add Paperclip to your Gemfile

    In Gemfile, add the following line:

    gem "paperclip", "~> 5.0.0"
  2. Create an Upload scaffold
    This will create app/controllers/uploads_controller.rb and app/views/index.html.erb among some other ones that we’re going to pretend don’t exist.

    rails g scaffold uploads title
  3. Add the File field to your Upload model
    This creates a file field & migration for your upload model

    rails g paperclip uploads file

    Add the following line to your app/models/uploads.rb. This will add the necessary processing and validation to your images.

    class Upload < ApplicationRecord
      has_attached_file :file, styles: { medium: "300x300>", thumb: "100x100>" }, default_url: "/images/:style/missing.png"
      validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/
    end

 

Install NPM Packages

  1. Install React-Dropzone
    npm install --save react-dropzone
  2. Install Superagent
    npm install --save superagent

Build Your Uploader Component

  1. Create your Uploader
    In your terminal, use the Rails generator to create app/javascript/components/Uploader.js

    rails g react:component Uploader csrf:string --es6

    The following will be generated…

    import React from "react"
    import PropTypes from "prop-types"
    class Uploader extends React.Component {
      render () {
        return (
          <div>
            <div>csrf: {this.props.csrf}</div>
          </div>
        );
      }
    }
    
    Uploader.propTypes = {
      csrf: PropTypes.string,
    };
    export default Uploader
  2. Add CSRF property
    In your Rails template for app/views/uploads/index.html.erb, change it to look something like this just to get things going. You might notice we’re using an Ruby loop over the @uploads resource. Sure, we could make GET request in our React component and display them within the tutorial, but for brevity we won’t, since the Uploading part is the non-trivial kinda-sorta hard part.

    <p id="notice"><%= notice %></p>
    
    <h1>Uploads</h1>
    
    <table>
      <thead>
        <tr>
          <th colspan="3"></th>
        </tr>
      </thead>
    
      <tbody>
        <% @uploads.each do |upload| %>
          <tr>
            <td><%= upload.title %></td>
            <td><%= image_tag upload.file.url(:thumb) %></td>
            <td><%= link_to 'Show', upload %></td>
            <td><%= link_to 'Edit', edit_upload_path(upload) %></td>
            <td><%= link_to 'Destroy', upload, method: :delete, data: { confirm: 'Are you sure?' } %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
    
    <br>
    
    <%= react_component("Uploader", { csrf: form_authenticity_token }) %>
    
  3. Test it out
    At this point you can probably run it to see if Webpack runs your code correctly.

    In one tab, make sure your Rails server is running with

    rails s

    In another tab, start Webpack

    webpack -w -d

    Note: If starting Webpack fails, you’ll need to update your bundle of Yarn

    yarn install
  4. Set it up for Dropzone

    Dropzone has a few really good examples on their site. Check them out here: https://react-dropzone.netlify.com/

    For this one, here’s our setup. Just a simple alert of how many files were accepted through the Drag N’ Drop’s validation and how many failed. I borrowed a lot of this from an example on there.

    import React from "react"
    import PropTypes from "prop-types"
    import Dropzone from 'react-dropzone'
    import request from "superagent";
    
    class Uploader extends React.Component {
    
      constructor() {
        super()
        this.state = {
          files: [],
        }
      }
    
    
      render () {
        return (
          <div>
            <div>Csrf: {this.props.csrf}</div>
            <Dropzone onDrop={this.onDrop.bind(this)}>
              {({ isDragActive, isDragReject, acceptedFiles, rejectedFiles }) => {
                if (isDragActive) {
                  return "This file is authorized";
                }
                if (isDragReject) {
                  return "This file is not authorized";
                }
                return acceptedFiles.length || rejectedFiles.length
                  ? `Accepted ${acceptedFiles.length}, rejected ${rejectedFiles.length} files`
                  : "Try dropping some files.";
              }}
            </Dropzone>
    
            <hr/>
            <h2>Dropped files</h2>
            <ul>
              {
                this.state.files.map(f => <li key={f.name}>{f.name} - {f.size} bytes</li>)
              }
            </ul>
          </div>
        );
      }
    
      onDrop(files) {
    
        this.setState({
          files
        });
      }
      
      }
    
    Uploader.propTypes = {
      csrf: PropTypes.string
    };
    export default Uploader
  5. Build the request
    In the onDrop function, add the following to create the request

    onDrop(files) {
    
        this.setState({
          files
        });
    
    
        files.map(f => {
    
          const fd = new FormData();
          fd.append("upload[title]", f.name);
          fd.append("upload[file]", f)
    
          request
            .post('/uploads/')
            .set('x-csrf-Token', this.props.csrf)
            .set('accept', 'json')
            .query({ format: 'json' })
            .send( fd ) // sends a JSON post body
            .end((err, res) => {
              // Calling the end function will send the request
            });
        })
    
    
      }

Conclusion

Again, this is a super small toy example. At this point, we should be able to click and drag files into the field to upload them. If you want to see the thumbnails and improve functionality, please see these links

If you’re a Rails person, you’ll probably have already noticed I generated a lot of unnecessary files and have a lot of unnecessary stuff going on. Just get rid of everything you don’t think you need — For example, as far as Templates go, you should only need your ERB template for app/views/uploads/index.html.erb, app/views/uploads/index.json.jbuilder.erb (if you want to do a GET request on it), app/views/uploads/show.json.jbuilder.

  • Yen Chi Tseng

    and don’t forget at upload_controller.rb add require :file