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:
- React
- React Apollo
- Tailwind CSS 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.
- Clone the part 1 repo
$ git clone git@github.com: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 recommendedInMemoryCache
andHttpLink
, 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: 'https://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!
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:
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.
The post How to Use GraphQL with Ruby on Rails – Part 2 appeared first on Web-Crunch.
Top comments (0)