RT Blog
Programming

Uploading content, React, GraphQL, Rails and Active Storage

In this tutorial, we will run through how to upload some data including an image using React and GraphQL, to a Rails backend server with Active Storage.

There are some great blog posts out there on all these topics but I found a lot to be out of date. The versions I’m using are below.

Rails6.0
React16.3.1
Apollo3.0.0

Rails

To start off with we will set up our backend rails server to accept GraphQL requests. Assuming you have a rails application already set up (if not details can be found here), we first need to add graphql to our project.

# Download the gem:
gem install graphql

# Setup with Rails:
rails generate graphql:install

Once installed it will create an app/graphql folder that contains two directories, one for mutations, and one for types. We will go over more about those later. First, we will create our model so we have something to add to the database.

We are going to create a simple Event model with three properties, name, date, and image. As the image will be handled by Active Storage, we can go ahead and create a model with those two fields for name and date.

# Generate Event model
rails generate model Event name:string start_date:date

# Update the database with new model
rails db:migrate

The image will be stored in the database using a tool called Active Storage. Active storage will allow us to store attachments to active record objects and then facilitate uploading those files to the cloud. For development purposes, the files will just be stored locally.

To install Active Storage first run

# Install Active Storage
rails active_storage:install

# Then run a db migration which will update the database with the Active Storage tables
rails db:migrate

Once the tables have been created we are ready to modify our model.

# app/models/event.rb

class Event < ApplicationRecord
    has_one_attached :image
    
    validates :name, presence: true
    validates :start_date, presence: true
end

In our Event model, we have added three new lines. First of all, we want to create an attachment to an Active Storage object. To do this we add the has_one_attached flag to our image property. The next two lines just verify that name and start_date must be present in all event entries. With our model in place, we are now ready to add types and mutations to our GraphQL backend.

GraphQL query

As mentioned earlier a new folder was added to the project when we added graphql. This is where we will do the rest of the backend work.

To start off with we are going to create a new type that will be used in the query and mutation requests. Below is a GraphQL query which will fetch a list of all our events.

query {
    events {
      id
      name
      startDate
      imageUrl
    }
}

This request called events will return the id, name, startDate, and imageUrl from our event type. To translate this into Rails we need to add a new query type.

#app/graphql/types/event_type.rb

module Types
  class Types::EventType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :start_date, GraphQL::Types::ISO8601DateTime, null: false
    field :image_url, String, null: true

  end
end

There are a few things going on here so I will go through them all. The first thing to notice is that we are calling this type EventType which has to match up to the filename. We then declare all the fields that can be used on this type. In our case, we supply four fields, id, name, start_date, and image_url. Notice these have to match up in the GraphQL query with the snake_case fields converted to camel case. After the names, we declare the field types. For start_date we want to return a date in ISO 8601 format so we use the GrapQL built-in type ISO8601DateTime. Finally, we supply a boolean value which states if the field can be null or not.

You might be wondering how the EventType ends up being converted into our Event model. Well, once we add a new field to the QueryType we will supply a function to fetch the model objects and pass them into the EventType. This then exposes an object property that we can use in the EventType to modify what is returned from the query. This is exactly what we are going to do for the image_url. As we don’t store the image_url directly on our Event model we need to fetch it from Active Storage. To do this we override the image_url by supplying a new function.

#app/graphql/types/event_type.rb

include(Rails.application.routes.url_helpers)

module Types
  class Types::EventType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :start_date, GraphQL::Types::ISO8601DateTime, null: false
    field :image_url, String, null: true

    def image_url
      if object.image.present?
        rails_blob_path(object.image, only_path: true)
      end
    end
  end
end

In our new image_url function we are going to first check if an image is present on our event. If it is, we then grab the path to the image using a url_heplers function rails_blob_path which will return the path to the image. Notice we also need to include the helper at the top.

That’s it for our EventType, we now need to add the events query inside QueryType.

#app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    field :events, [EventType], null: false
 
     def all 
        Event.all.with_attached_image
    end
  end
end

The QueryType is the first entry point for our GraphQL request. Here we can declare all the different query types we want. For our example, we just want to return a list of events. For that, we need to add just one field, which must match the GraphQL query name. In our case, we have called it events. We also supply a type for our field, which is an array of EventTypes, and also a boolean which states if it can be null or not.

Finally, we need to fetch our event models for our query. To do that we create an events function and just return all the events in our database. As we want the Active Storage data as well, we need to fetch all the events with the attached image.

Congratulations we now have a fully working query setup in Rails using GraphQL.

If we run this query we will see a list of all the events that we have currently stored in the database.

query {
    events {
      id
      name
      startDate
      imageUrl
    }
}

One small problem. We don’t have any entries in the database yet. To create new entries we will need to create a new GraphQL mutation.

GraphQL mutations

To add objects to our database we will need to create a GraphQL mutation. Not only that but as we are uploading an image, we will need to leverage some third-party libraries to help us out.

For GraphQL requests, we are going to use a tool called Apollo. When it comes to queries and simple requests Apollo has everything we need. However, once we start uploading files we need a little more help. To upload an image we will have to create a multipart request so we can upload the file along with the other data. To do that we need to use a gem called Apollo Upload Server. So let’s go ahead and install that.

# add to Gemfile
gem 'apollo_upload_server', '2.0.1'

Once we have added the gem we can go ahead and start creating our mutation.

#app/graphql/mutations/add_event.rb

module Mutations
    class AddEvent < Mutations::BaseMutation
        argument :name, String, required: true
        argument :start_date, String, required: true
        argument :image, ApolloUploadServer::Upload, required: true
    end
end

We start off by declaring the arguments our mutation will take. We want to upload a name and a date, both as strings. We also want an image which is of type ApolloUploadServer::Upload. This type is from the Apollo upload server and will do all the work of parsing the multipart request and return us a file we can use. Next, we need to add a resolve function to create our event.

We’re also going to declare the fields that can be returned in our GraphQL mutation request.

#app/graphql/mutations/add_event.rb

…

field :event, Types::EventType, null: true
field :errors, [String], null: true    

def resolve(input)
    file = input[:image]
    blob = ActiveStorage::Blob.create_and_upload!(
        io: file,
        filename: file.original_filename,
        content_type: file.content_type
    )

    event = Event.new(
        name: input[:name],
        start_date: input[:start_date],
        image: blob
    )
    
    if event.save 
        { event: event } 
    else 
        { errors: event.errors.full_messages }
    end
end

…

The first field is going to be our EventType that we made earlier. If the request is successful an event will be returned. The second field is our errors, which will return any errors generated from saving our event object. Like the query, these two field names need to match the names given in the mutation. We will look at what the raw mutation request looks like very shortly.

Finally, we create a resolve function that takes in our data from the GraphQL request and creates a new event object. For the image, we need to create an attachment so that it can be linked to the model in Active Storage. To create the attachment we use ActiveStorage::Blob.create_and_upload! to generate a blob from the file we have imported, and then pass it into our event. Once the object has been saved we assign it to our event field so that it can be returned in the GraphQL request.

Before we can test it out we just need to add a field to our MutationType.

#app/graphql/types/mutation_type.rb

module Types
  class MutationType < Types::BaseObject
    field :add_event, mutation: Mutations::AddEvent
  end
end

Like our QueryType, this will be our entry point for the mutation. We call it add_event and set the type to the mutation we just created called AddEvent.

We are ready to go! We can finally try out our new API and see event entries being added to the database.

To test our addEvent mutation will need to send a POST request using multipart. I’m using Insomnia but feel free to use any rest client that supports multipart requests.

Here we can see that the request is a success and a new event has been added. Let’s break this down to see exactly what is going on.

We need to send three parts in the multipart fields to get this request to work. The first one is the operations, which is a raw string of the GraphQL request.

{
  "query": "mutation ($name: String!, $startDate: String!, $image: Upload!) { addEvent( input: { name: $name startDate: $startDate image: $image }) { event { id name startDate imageUrl } errors } }", "variables": { "name": "MultiTest2", "startDate": "13/04/2019", "image": null } 
}

In this string, we supply a query and some variables. The query part is the GraphQL mutation, which when formatted, looks like this.

mutation ($name: String!, $startDate: String!, $image: Upload!) {
    addEvent(
        input: {
           name: $name
           startDate: $startDate
           image: $image
        }
    ) { 
        event { 
            id
            name
            startDate
            imageUrl
        }
        errors
    }
}

Here we can see more clearly exactly what we need to pass in. We first say it is a mutation and pass in the three types we need. The image is of type Upload which will match the Upload type from the Apollo upload server. Next, we call the add_event mutation which we created earlier. Remember snake_case is converted to camel case in GraphQL so that is why it looks a little different. After that, we supply the input values, which in our case is the name, startDate, and image. Finally, we supply the return values, similar to the query.

The final part of the operation is the variables, which is a hash of all the variables we are including in the request. Notice image is set to null as that will be included in a separate part of the request.

The second part of the multipart request is the map field.

Here we provide the name of all files that are going to be uploaded. The key is the name of the final part and the value is the name of the variable we declared in the operations field.

The final part of the request is the actual file we want to pass in. The name has to match the key in the map field. We can attach any file, in this case, we have passed in an image called profile.jpeg.

Congrats on getting this far. We now have a complete backend system set up to handle our GraphQL requests. The next step is to link it up with our React frontend.


React

To facilitate the GraphQL requests we are going to use a very popular framework called Apollo.

Before we start writing our components we are first going to set up Apollo and the Apollo upload client, which will handle the GraphQL requests and file upload.

yarn add @apollo/client
yarn add apollo-upload-client

Assuming we have a React project already set up we are first going to set up our apolloClient.

// src/utils/apollo/apolloClient.js

import { ApolloClient, InMemoryCache } from '@apollo/client';
import { createUploadLink } from 'apollo-upload-client'

const link = createUploadLink({
    uri: "localhost:3001" + 'graphql'
});
const client = new ApolloClient({
    cache: new InMemoryCache(),
    link: link
});

export default client;

We just have some boilerplate code here for our Apollo client. We need to make sure we add the correct URL. In this case, we are running on port 3001 as our react instance will be running on port 3000.

To pass in the client we will need to inject it into our root index.js.

// src/index.js

import React from 'react';
import { ApolloProvider } from '@apollo/client';
import App from './components/App';
import client from './utils/apollo/apolloClient'

ReactDOM.render(
  	<ApolloProvider client={client}>
  	  <App />
  	</ApolloProvider>
  document.getElementById('root')
);

Here we pass in the client we created before as part of the ApolloProvider prop. This will give us access to the client when we use the useMutation hook later on.

We can now create some components. The first will allow us to drop a file on the page ready to be submitted. We are going to use a third-party library called react-dropzone for this, so let’s add it.

yarn add react-dropzone

Next, we can add some code for our Dropzone component.

import React, { useMemo, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

const baseStyle = {
    flex: 1,
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    padding: '20px',
    borderWidth: 2,
    borderRadius: 2,
    borderColor: '#eeeeee',
    borderStyle: 'dashed',
    backgroundColor: '#fafafa',
    color: '#bdbdbd',
    outline: 'none',
    transition: 'border .24s ease-in-out'
};

const activeStyle = {
    borderColor: '#2196f3'
};

const acceptStyle = {
    borderColor: '#00e676'
};

const rejectStyle = {
    borderColor: '#ff1744'
};

function Dropzone(props) {
    
    // onDrop returns a function that will return our file.
    const onDrop = useCallback(
        ([file]) => {
            props.handleFile(file);
        }
    );
    
    const {
        acceptedFiles,
        getRootProps,
        getInputProps,
        isDragActive,
        isDragAccept,
        isDragReject
    } = useDropzone({ accept: 'image/*', onDrop });

    const files = acceptedFiles.map(file => (
        <li key={file.path}>
            {file.path} – {file.size} bytes
        </li>
    ));

    const style = useMemo(() => ({
        …baseStyle,
        …(isDragActive ? activeStyle : {}),
        …(isDragAccept ? acceptStyle : {}),
        …(isDragReject ? rejectStyle : {})
    }), [
        isDragActive,
        isDragReject
    ]);

    return (
        <section className="container">
            <div {…getRootProps({ style })}>
                <input {…getInputProps()} />
                <p>Drag 'n' drop some files here, or click to select files</p>
            </div>
            <aside>
                <h4>Files</h4>
                <ul>{files}</ul>
            </aside>
        </section>
    );
}

export default Dropzone;

Here we are creating a styled Dropzone component that will display the name of the file that has been imported as shown below.

We are exposing a handleFile function which can be imported through our component’s props.

We now need to write another component that will use our Dropzone component.

import React, { useMemo, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

const baseStyle = {
    flex: 1,
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    padding: '20px',
    borderWidth: 2,
    borderRadius: 2,
    borderColor: '#eeeeee',
    borderStyle: 'dashed',
    backgroundColor: '#fafafa',
    color: '#bdbdbd',
    outline: 'none',
    transition: 'border .24s ease-in-out'
};

const activeStyle = {
    borderColor: '#2196f3'
};

const acceptStyle = {
    borderColor: '#00e676'
};

const rejectStyle = {
    borderColor: '#ff1744'
};

function Dropzone(props) {
    
    // onDrop returns a function that will return our file.
    const onDrop = useCallback(
        ([file]) => {
            props.handleFile(file);
        }
    );
    
    const {
        acceptedFiles,
        getRootProps,
        getInputProps,
        isDragActive,
        isDragAccept,
        isDragReject
    } = useDropzone({ accept: 'image/*', onDrop });

    const files = acceptedFiles.map(file => (
        <li key={file.path}>
            {file.path} – {file.size} bytes
        </li>
    ));

    const style = useMemo(() => ({
        …baseStyle,
        …(isDragActive ? activeStyle : {}),
        …(isDragAccept ? acceptStyle : {}),
        …(isDragReject ? rejectStyle : {})
    }), [
        isDragActive,
        isDragReject
    ]);

    return (
        <section className="container">
            <div {…getRootProps({ style })}>
                <input {…getInputProps()} />
                <p>Drag 'n' drop some files here, or click to select files</p>
            </div>
            <aside>
                <h4>Files</h4>
                <ul>{files}</ul>
            </aside>
        </section>
    );
}

export default Dropzone;

So here we have our CreateEvent component which will use the mutation request to add an event to our database. We just have a simple input form that exposes a name, date, and file through the Dropzone component we created earlier.

Notice we are using the useMutation hook which is provided through the Apollo client to fetch our mutation. We can now go ahead and create that mutation.

// src/utils/apollo/queries/addEvent

import gql from "graphql-tag";

const ADD_EVENT = gql`
mutation ($name: String!, $startDate: String!, $image: Upload!) {
    addEvent(
        input: {
            name: $name
            startDate: $startDate
            image: $image  
        }
    ) { 
        event { 
            id
            name
			startDate
			imageUrl
        }
        errors
    }
}
`;

export default ADD_EVENT

This should look familiar as this is the same mutation that we added to the query section of the multipart request.

We then call the function returned from the mutation hook, and pass in the variables that it needs, which in our case is the name, date, and image file.


And there we have it, our frontend will now create an event using the name, date, and image, by sending the data using GraphQL to our Rails backend. We can check the request was successful by looking at the logs from our Rails server, or by running our events query and see the newly created fields.

Bonus: Try and create a list in React showing all of the events that have been added to the database. You will need to use the GrapgQL query we setup earlier.

Related posts

Method Swizzling

Rowan
5 years ago

Full-stack introduction using Rails, React, Docker and Heroku

Rowan
5 years ago

Property Wrappers

Rowan
5 years ago
Exit mobile version