Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

November 29, 2019

Last updated November 5, 2023

How to Use GraphQL with Ruby on Rails - Part 2

Continuing on from Part 1 is the frontend portion of the tutorial. I'll leverage React, Apollo and Tailwind CSS to build out the frontend of our Ruby on Rails and GraphQL API application.

The tools I'm reaching for include the following:

Download the source code

Carrying over from Part 1

Important note: I made an entire copy of the original app and created a new Github repo for you to download/reference. So if you're coming from Part 1 you either need to carry on from it or clone the new repo.

Here are the steps I took to get the Rails API app up and running.

  1. Clone the part 1 repo
$ git clone [email protected]:justalever/graphql_fun.git graphql_fun_frontend
$ cd/graphql_fun_frontend
$ bundle install
$ rails db:migrate
$ rails db:seed
$ rails server

The commands above should get you a booted Ruby on Rails API application with some seeded data to query with GraphQL.

Part 2 Setup

You could potentially separate your front-end completely from this project and have two separate apps communicating in tandem. We'll be doing this but I'll house the frontend app within the same repo as the Ruby on Rails app. Version control becomes a touch easier in my opinion for this but it also mixes concerns. To each their own so approach that how you wish.

Rails API

For our front-end app to communicate "securely " with the Rails API app we need to add a new gem called rack-cors. It should be commented out in your Gemfile at this point. Uncomment it and run bundle install

# Gemfile
gem 'rack-cors'

Then, inside your config/initializers/cors.rb file you can uncomment the code there to match the following:

# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Important: When pushing this to a production environment you will want to change the origins to whatever remote domains your app lives on i.e. (origins 'web-crunch.com', 'staging.web-crunch.com') and so on.

React Frontend

Now on to the frontend portion. If you've been around the frontend scene for any amount of time recently you've probably heard of React. I won't go into heavy detail of what React is or why you should/shouldn't use it but rather direct you to the docs to see the benefits.

I personally am more of a Vue.js fan but React certainly has a large fan base.

All that aside, we'll make use of create-react-app to get things set up pretty darn fast.

$ yarn global add create-react-app

I added the create-react-app module bundle globally so we could reference for other projects later. Consider this optional for your own system.

$ create-react-app frontend
$ cd frontend
$ yarn start

You may get a noticed about port 3000 being already in use. It will prompt you to use an alternate. I went ahead and said yes to the command. My frontend app now runs on localhost:3001 in another browser tab.

To get a visual of the current directory structure I like to make use of tree.

On a mac you can run brew install tree to use it. Passing an -I plus a string of folders/files will ignore those.

$ tree . -I 'node_modules'
.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   └── serviceWorker.js
└── yarn.lock

2 directories, 16 files

A few notes:

  • I'm not going to be worrying about front-end tests here for brevity sake
  • We can delete the logo images and svgs since we'll use our own assets

Add Tailwind CSS

We need some dependencies installed to get Tailwind CSS dev-ready.

$ yarn add tailwindcss

$ yarn add postcss-cli autoprefixer -D // Save for dev use only

Initialize a config file:

$ yarn tailwind init --full

This generates a default tailwind.config.js file with the default scheme thanks to the --full flag.

Inside index.css lets scrap everything and add the tailwind directives.

/* frontend/index.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

Add a postcss.config.js file within frontend

// frontend/postcss.config.js

module.exports = {
  plugins: [
    require('tailwindcss')('tailwind.config.js'),
    require('autoprefixer'),
  ]
};

Let's update our package.json scripts section to account for Tailwind

 "scripts": {
    "build:style": "tailwind build src/index.css -o src/tailwind.css",
    "start": "yarn build:style && react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

Your results may vary here depending on your own folder structure. The general idea is that we'll add styles to index.css and output those to tailwind.css as compiled styles.

If your server is running at this point you should restart it:

$ yarn start

My updated frontend folder structure now looks like the following:

# graphql_fun_frontend/frontend 
$ tree . -I 'node_modules'
.
├── README.md
├── package.json
├── postcss.config.js
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── components
│   │   ├── App.js
│   │   └── Users.js
│   ├── index.css
│   ├── index.js
│   ├── serviceWorker.js
│   └── tailwind.css
├── tailwind.config.js
└── yarn.lock

3 directories, 15 files

Be sure to update your main index.js imports and components/App.js file. Notice I made a components folder for better organization as well. This is just a preference.

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './tailwind.css';
import App from './components/App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

And the App.js file

// frontend/src/components/App.js
import React from 'react';

function App() {
  return (
    <div className="App">

    </div>
  );
}

export default App;

Apollo

You may ask why Apollo? My answer is...mostly because it's the easier/faster solution to querying GraphQL via the front-end. Are there other approaches out there? I'm 100% sure there are but the Apollo team are what I'd consider the pioneers of the approach. We'll follow their conventions in this tutorial.

I'll be leveraging:

  • react-apollo - A React port for using Apollo within components.
  • apollo-boost - Apollo Boost is a zero-config way to start using Apollo Client. It includes some sensible defaults, such as our recommended InMemoryCache and HttpLink, which come configured for you with our recommended settings.
  • graphql - GraphQL itself
$ yarn add react-apollo apollo-boost graphql

After those are installed we can extend frontend/src/index.js to include the following:

// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './tailwind.css';
import App from './components/App';
import * as serviceWorker from './serviceWorker';
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';

const link = createHttpLink({
  uri: 'http://localhost:3000/graphql' // This is relative to our Rails API port running on 3000
});

const client = new ApolloClient({
  link: link,
  cache: new InMemoryCache()
});

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

serviceWorker.unregister();

With the client now passed down from index.js, we can start writing GraphQL queries. Let's start with a Users.js component. Create a new file src/components/Users.js. Within that file import the following.

// src/components/Users.js
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import Gravatar from 'react-gravatar';

We added one more dependency here for Gravatars.

$ yarn add react-gravatar # a handy gravatar package

Next, we can build a familiar query from Part 1. The file then becomes a bit longer.

// src/components/Users.js
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import Gravatar from 'react-gravatar';

const GET_USERS = gql`
  {
    users {
      id
      name
      email
      postsCount
    }
  }
`;

Finally, we can build our Users component and pipe in the data. We'll leverage Tailwind CSS for styling here. This also makes use of React hooks.

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import Gravatar from 'react-gravatar';

const GET_USERS = gql`
  {
    users {
      id
      name
      email
      postsCount
    }
  }
`;

function Users() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return 'Loading...';
  if (error) return `Error ${error.message}`;

  return (
    <div className="flex flex-wrap items-center">
      {data.users.map(user => (
        <div class="lg:w-1/3 w-full p-4 border" key={user.id}>
          <Gravatar email={user.email} size={150} className="w-full" />
          <h3 className="font-bold text-xl">{user.name}</h3>
          <p className="text-gray-500">{user.email}</p>
          <p className="text-gray-500">{user.postsCount} posts</p>
        </div>
      ))}
    </div>
  );
}

export default Users;

Within it, we destructure { loading, error, data } variables for use. The main one being data which is what comes back thanks to our GraphQL query.

To actually render this component we need to import it inside App.js

// frontend/src/components/App.js
import React from 'react';
import Users from './Users';

class App extends React.Component {
  render() {
    return (
      <div className="container mx-auto px-4">
        <Users />
      </div>
    );
  }
}

export default App;

That gets us some basic stuff in the view!

https://i.imgur.com/57qeTMY.png

User Profile & Posts View

Let's create a singular profile page called User.js inside src/components/User.js. I'll be using React Hooks where possible as we digress a bit further in creating more components. You can opt for the traditional React component approach as well. You'll find I mix and match a bit.

For our User component, I went ahead and cleaned up a bit of code to extract some bits into smaller components. The UserAvatar component now can be used everywhere we want it as a result. It accepts a user prop.

First, we need to import those dependencies and components.

// frontend/src/components/User.js
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import UserAvatar from './UserAvatar';
import Posts from './Posts';

Then add the gql query

// frontend/src/components/User.js

const GET_USER = gql`
  query User($id: ID!) {
    user(id: $id) {
      posts {
        id
        title
      }
    }
  }
`;

And finally, the React Hook itself

function User({ user, selectUser }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: user.id }
  });

  if (loading) return 'Loading...';
  if (error) return `Error ${error.message}`;

  return (
    <React.Fragment>
      <div className="flex flex-wrap my-4">
        <button
          className="bg-gray-200 hover:bg-gray-400 text-gray-900 font-bold py-2 px-4 rounded"
          onClick={selectUser.bind(this, null)}>
          Back
        </button>
      </div>
      <div className="flex flex-wrap items-start mb-4">
        <div className="lg:w-1/4 w-full rounded text-center">
          <UserAvatar user={user} />
        </div>
        <div className="px-4 flex-1 w-full">
          <Posts posts={data.user.posts} user={user} />
        </div>
      </div>
    </React.Fragment>
  );
}

export default User;

There is some code we reference here that hasn't been addressed yet so let's do that now.

// frontend/src/components/UserAvatar.js

import React from 'react';
import Gravatar from 'react-gravatar';

const UserAvatar = ({ user }) => (
  <React.Fragment>
    <Gravatar email={user.email} size={200} className="rounded-full text-center inline" />
    <div className="px-6 py-4">
      <div className="font-bold text-xl mb-2">{user.name}</div>
      <p className="text-gray-500 text-sm">{user.email}</p>
      <p className="text-gray-500 text-base">{user.postsCount} posts</p>
    </div>
  </React.Fragment>
)
export default UserAvatar;

Above is the UserAvatar component. It wraps our react-gravatar import into a nice reusable package for us.

// frontend/src/components/Posts.js
import React from 'react';

function Posts({ posts, user }) {
  return (
    <React.Fragment>
      <div className="lg:pl-10">
        <h1 className="font-bold mb-4">Posts from {user.name}</h1>
        {posts.map(post => (
        <div key={post.id}>
          <div className="p-6 shadow mb-4">
            <h3 className="text-2xl font-bold text-gray-800">{post.title}</h3>
          </div>
        </div>
      ))}
      </div>
    </React.Fragment>
  );
}

export default Posts;

Next is the Posts component which accounts for the rendering of each user's posts.

Update the main App.js Component

// frontend/src/components/App.js
import React from 'react';
import User from './User';
import Users from './Users';

class App extends React.Component {
  state = {
    selectedUser: null
  }

  selectUser = (user) => {
    this.setState({ selectedUser: user })
  }

  render() {
    return (
      <div className="container mx-auto px-4">
        {this.state.selectedUser ?
        <User user={this.state.selectedUser} selectUser={this.selectUser} /> :
        <Users selectUser={this.selectUser} />}
      </div>
    );
  }
}

export default App;

Here we use a traditional React component and some state to manage if a user is indeed selected. If there's an onClick fired we see a User profile instead of the Users listing.

Create a User

Creating a user requires GraphQL Mutations. Our approach will be similar to our other components with a few variances.

Create a new component called CreateUser.js. Inside I added the following:

import React, { Component } from 'react';
import gql from "graphql-tag";
import { Mutation } from "react-apollo";

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(input: { name: $name, email: $email }) {
      user {
        id
        name
        email
        postsCount
      }
      errors
    }
  }
`;

class CreateUser extends Component {
  state = {
    name: '',
    email: ''
  }

  onSubmit = (e, createUser) => {
    e.preventDefault();
    createUser({ variables: this.state });
    this.setState({ name: '', email: '' });
  }

  render() {
    return (
      <Mutation
        mutation={CREATE_USER}
        update={this.props.onCreateUser}>
        {createUserMutation => (
          <div className="lg:fixed bottom-0 left-0 w-full bg-white border-t border-gray-300">
            <form className="lg:px-8 pt-2 pb-2" onSubmit={e => this.onSubmit(e, createUserMutation)}>
                <div className="lg:flex flex-wrap flex-between items-center justify-center lg:p-0 p-6">
                  <h4 className="font-bold lg:pr-4 mb-2">Create new user</h4>
                  <div className="lg:pr-4 mb-2">
                    <input
                      className="border rounded w-full py-2 px-3"
                      type="text"
                      value={this.state.name}
                      placeholder="Name"
                      onChange={e => this.setState({ name: e.target.value })} />
                  </div>
                  <div className="lg:pr-4 mb-2">
                    <input
                      className="border rounded w-full py-2 px-3"
                      type="email"
                      value={this.state.email}
                      placeholder="Email"
                      onChange={e => this.setState({ email: e.target.value })} />
                  </div>
                  <button
                    className="bg-blue-500 text-white py-2 px-4 rounded"
                    type="submit">
                    Create User
                  </button>

              </div>
            </form>
          </div>
        )}
      </Mutation>
    );
  }
}

export default CreateUser;

I chose to use traditional React render props instead of React hooks for this component. Being newer to react this version made more sense to me. We're setting some state relative to the User object. To create a new user we need an email and name. Adding those happen on the frontend with a form. Using state we can capture events onChange to fire the setState method.

When the form is submitted we call a method createUser where we pass in the state. Once the state updates our GraphQL mutation is finally called.

In the end, the UI looks like the following:

Screenshot_2019-11-19 How to use GraphQL with Ruby on Rails - Part 2 Frontend

The form is fixed to the bottom of the browser window but you can see I've added a couple of my own accounts with gravatar images.

Wrapping Up

We've come a long way. GraphQL + React + Ruby on Rails can be a very powerful combo. I invite you to extend this app to account for creating posts as well. You'll need to add new queries on both the backend and frontend to achieve this result.

If you followed along this far, I can't thank you enough. Be sure to check out my other content as well as my YouTube channel to see more videos there.

If you're brand new to Ruby on Rails I also created a full course on it called Hello Rails. It's 90 videos of jam-packed knowledge about the awesome framework I use every day.

Link this article
Est. reading time: 13 minutes
Stats: 3,559 views

Categories

Collection

Part of the Ruby on Rails collection

Products and courses