This is a continuation of Rails & GraphQL Part 1, where we built the backend of our API in rails. This article will cover integrating React and Apollo (a JavaScript GraphQL framework) into the rails API. If you haven't read part 1 yet, please do so now!
Rails API Setup
In order to use React with rails, we need to make some changes to the API.
# Gemfile
gem 'rack-cors'
Then, open up application.rb
and add the following settings. This will allow us to accept AJAX requests coming from React.
# config/application.rb
# CORS config to allow ajax
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :options]
end
end
Note: you'll want to replace *
with your domain if you're in a production environment!
Extra Seed Data
I added some extra data to the Book
model so we'll have more to display in our frontend app. View commit
# app/graphql/types/book_type.rb
module Types
class BookType < Types::BaseObject
field :id, Integer, null: false
field :title, String, null: false
field :cover_url, String, null: true
field :average_rating, Integer, null: true
end
end
React
In the Rails app folder, we're going to create a new react app using create-react-app
. See their README for setup guides. I'm using yarn
here, but you can use npm
if you'd like.
create-react-app frontend
cd frontend
yarn start
I deleted some the css, svg, and tests generated with create-react-app, as well as created components
and styles
folders to better organize things. Here's what my project looks like:
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── components
│ │ └── App.js
│ ├── index.js
│ ├── serviceWorker.js
│ └── styles
│ └── index.css
├── package.json
└── yarn.lock
Be sure to remove references of App.css
and update the paths to point to ./components/App
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './styles/index.css';
import App from './components/App'; // Updated path
import * as serviceWorker from './serviceWorker';
// src/components/App.js
import React, { Component } from 'react';
-import logo from '../logo.svg';
-import '../styles/App.css';
Styling
For some quick styling, add a link to TailwindCSS.
// src/index.html
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
For user avatars, we'll use Gravatar with yarn add react-gravatar
.
Apollo
react-apollo
is a React-specific library for using Apollo in componentsapollo-boost
contains many utilities and libraries for Apollo to get you set up quickly- and finally the javascript
graphql
library itself
yarn add apollo-boost react-apollo graphql
Setting up ApolloClient
We need to configure ApolloClient
with our API and wrap our root <App />
with the ApolloProvider
higher-order component.
// src/index.js
// [truncated]
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'
});
const client = new ApolloClient({
link: link,
cache: new InMemoryCache()
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
Users Index View
Now that we have the client passed down from index.js
, we can start writing queries. Create a new component in src/components/Users.js
and import the following:
// src/components/Users.js
import React, { Component } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
import Gravatar from 'react-gravatar';
Query
is another higher-order component that we'll wrap all of ourUsers
index page withgql
will help us build queries to send to the APIGravatar
is for quick user avatars
Now we can write our query to fetch users
and return id, name, email, booksCount
(just like the test queries against the Rails server using graphiql!). Place it after the imports but before the class definition.
// Below imports in src/components/Users.js
const USERS_QUERY = gql`
query {
users {
id
name
email
booksCount
}
}
`;
And finally, we have the Users index component class with its render function. The Query
component is passed the USERS_QUERY
and it returns with loading states, error info (if any), and the API's response data rendered in jsx. Since this component is wrapped in Query
, the API request will be sent immediately after it's rendered. Some styling from Tailwind has also been added to render users inside cards.
// Below user query in src/components/Users.js
class Users extends Component {
render() {
return (
<Query query={USERS_QUERY}>
{({ loading, error, data }) => {
if (loading) return <div>Fetching..</div>
if (error) return <div>Error!</div>
return (
<div className="flex flex-wrap mb-4">
{data.users.map((user) => {
return <div key={user.id} className="m-4 w-1/4 rounded overflow-hidden shadow-lg">
<Gravatar email={user.email} size={150} className="w-full" />
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2">{user.name}</div>
<p className="text-grey-darker text-base">{user.email}</p>
<p className="text-grey-darker text-base">{user.booksCount} books</p>
</div>
</div>
})}
</div>
)
}}
</Query>
)
}
}
export default Users;
To see this component in action, we need to call it from our app's root component App.js
// src/components/App.js
import React, { Component } from 'react';
import Users from './Users';
class App extends Component {
render() {
return (
<div className="container mx-auto px-4">
<Users />
</div>
);
}
}
export default App;
User Profile & Books View
Next, we'll need to create a user's profile page which includes their list of books. Since the index page only returns exactly what it needed to render, we'll need to send a new request to the API to get back all that user's data.
// src/components/User.js
import React, { Fragment } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
import UserAvatar from './UserAvatar';
import Books from './Books';
const USER_QUERY = gql`
query User($id: ID!) {
user(id: $id) {
books {
id
title
coverUrl
averageRating
}
}
}
`;
// src/components/User.js
const User = ({ user, selectUser }) => (
<Query query={USER_QUERY} variables={{ id: user.id }}>
{({ loading, error, data }) => {
if (loading) return <div>Fetching..</div>
if (error) return <div>Error!</div>
return (
<Fragment>
<div className="flex my-4">
<button
className="bg-grey-light hover:bg-grey text-grey-darkest font-bold py-2 px-4 rounded"
onClick={selectUser.bind(this, null)}>
Back
</button>
</div>
<div className="flex mb-4">
<div className="my-4 w-1/4 rounded overflow-hidden">
<UserAvatar user={user} />
</div>
<div className="my-4 px-4 w-3/4">
<Books books={data.user.books} />
</div>
</div>
</Fragment>
)
}}
</Query>
);
export default User;
User Avatar
In this User
component, I've added a UserAvatar
and a Books
component to help display the data in a reusable way. I'll also refactor the user avatar code on the Users index page to use the new component.
// src/components/UserAvatar.js
import React, { Fragment } from 'react';
import Gravatar from 'react-gravatar';
const UserAvatar = ({ user }) => (
<Fragment>
<Gravatar email={user.email} size={150} className="w-full" />
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2">{user.name}</div>
<p className="text-grey-darker text-sm">{user.email}</p>
<p className="text-grey-darker text-base">{user.booksCount} books</p>
</div>
</Fragment>
)
export default UserAvatar;
// ....
// src/components/Users.js
<div className="flex flex-wrap mb-4">
{data.users.map((user) => {
return <div key={user.id}
className="m-4 w-1/4 rounded overflow-hidden shadow-lg"
onClick={this.props.selectUser.bind(this, user)}>
<UserAvatar user={user} />
</div>
})}
</div>
// ...
Books
// src/components/Books.js
import React, { Fragment } from 'react';
const Books = ({ books }) => (
<Fragment>
{books.map((book) =>
<div key={book.id} className="flex border-b border-solid border-grey-light">
<div className="w-3/4 p-4">
<h3>{book.title}</h3>
<p className="text-grey-darker">
{[...Array(book.averageRating).keys()].map((s) =>
<span key={s}>★</span>
)}
</p>
</div>
<div className="w-1/4 p-4 text-right">
<img src={book.coverUrl} alt={book.title} />
</div>
</div>
)}
</Fragment>
);
export default Books;
App
Then, we must call the User component from our main App component. In here, we can hook up the action to show & hide user profiles on click. We're also storing the selected customer profile in the state object.
// src/components/App.js
import React, { Component } from 'react';
import Users from './Users';
import User from './User';
class App extends 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;
Creating a New User
To create users via mutations, we need a new component in src/components/CreateUser.js
. Here we will need to import the Apollo Mutation
and write the query.
// src/components/CreateUser.js
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
booksCount
}
errors
}
}
`;
Then, we define our component and its initial state.
// src/components/CreateUser.js
class CreateUser extends Component {
state = {
name: '',
email: ''
}
onSubmit = () => {
// We'll implement this later
}
render() {
return (
<Mutation mutation={CREATE_USER}>
<!-- implemented later -->
</Mutation>
);
}
}
export default CreateUser;
The full mutation
<Mutation
mutation={CREATE_USER}
update={this.props.onCreateUser}>
{createUserMutation => (
<form className="px-8 pt-6 pb-8 mb-4" onSubmit={e => this.onSubmit(e, createUserMutation)}>
<h4 className="mb-3">Create new user</h4>
<div className="mb-4">
<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="mb-6">
<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 text-white py-2 px-4 rounded"
type="submit">
Create
</button>
</form>
)}
</Mutation>
Submitting the form
onSubmit = (e, createUser) => {
e.preventDefault();
createUser({ variables: this.state });
this.setState({ name: '', email: '' });
}
Rendering the CreateUser
component
// src/components/Users.js
import CreateUser from './CreateUser';
// ....
<div className="flex flex-wrap mb-4">
<Fragment>
{data.users.map((user) => {
// truncated
})}
<div className="m-4 w-1/4 rounded overflow-hidden shadow-lg">
<CreateUser onCreateUser={this.updateUsers} />
</div>
</Fragment>
</div>
Dynamically updating the cache for the list of users on the index page
// ....
// src/components/Users.js
class Users extends Component {
updateUsers = (cache, { data: { createUser } }) => {
const { users } = cache.readQuery({ query: USERS_QUERY });
cache.writeQuery({
query: USERS_QUERY,
data: { users: users.concat([createUser.user]) },
});
}
Learning all this tech at once is a lot to take in, and it's difficult to package it all together in an easy to digest way. I hope these tutorials helped point you in the right direction. Feel free to reach out if you have any questions about what I went over here!