GraphQL Client Frameworks: Comparing Relay Modern & Apollo
For some time now we've been eying GraphQL as a potential technology to add to our stack for upcoming projects here at SmartLogic. GraphQL is a query language for APIs and a runtime that sits atop your existing server to fulfill those queries. GraphQL queries, unlike REST, allow you to ask for all the data you want at once, and GraphQL is also strongly typed, which is a huge plus for me and my fellow developers at SmartLogic.
We typically use React for major frontend work at SmartLogic, and there are two bigger client libraries that combine GraphQL with React: Apollo, the more simple and yet flexible approach, and Relay Modern, Facebook's more structured and opinionated approach. In this post, I'll be comparing the two.
Why Use a GraphQL Client Library?
Using a GraphQL Client Library can help assist with a few different areas that would otherwise take a lot of time to develop. A library will:
- construct the HTTP request for you, requiring only that you provide it with a single setup configuration and your query with variables;
- make it easier to plug middlewares and afterwares such as an authentication layer;
- make GraphQL subscriptions a cinch, which allows the client to be notified when data is changed on the server;
- enable caching to reduce the number of queries required and keep UI consistent; and
- allow us to strongly integrate queries with components in the case of a React library.
Relay Modern
Relay Modern has a much higher learning curve than other GraphQL client libraries. The reason for this is because Facebook has a very specific structure it wants you to follow when using the framework.
Containers
One of the parts of this structure is colocating GraphQL fragments with React components. Relay calls this containers. Here is an example of a container for a Post component:
import {
createFragmentContainer,
graphql
} from 'react-relay'
class Post extends Component {
render() {
return (
<div>{this.props.post.description}</div>
)
}
}
export default createFragmentContainer(Post, graphql`
fragment Post_post on Post {
id
title
body
}
`)
We use the Higher Order Component createFragmentContainer
to combine the Post component with our query to obtain information about a Post. Fragments must also be named, specifically with the name of the file (Post.js
gives us Post
) and the prop that gets passed into the Post component (post
in this case). createFragmentContainer
adds the post
prop now to the Post component, allowing you to access the data (id
, title
, and body
in this case) through that prop.
We then can use this Post_post
fragment in a Post List component that contains many Post components.
import {
createFragmentContainer,
graphql
} from 'react-relay'
class ListPage extends Component {
render() {
return (
{this.props.viewer.allPosts.edges.map(({node}) =>
<Post key={node.__id} post={node} />
)}
)
}
}
export default createFragmentContainer(ListPage, graphql`
fragment ListPage_viewer on Viewer {
allPosts(last: 100, orderBy: createdAt_DESC) @connection(key: "ListPage_allPosts", filters: []) {
edges {
node {
...Post_post
}
}
}
You can see that we are using the Post_post
fragment inside of this new ListPage_viewer
fragment. In Relay Modern, all fragments are in scope, so you can use any fragment anywhere you like without needing to import any code.
GraphQL Relay Specification
You may notice that the GraphQL used above uses some key terms that may look unfamiliar such as viewer
, edges
, and node
. This is the GraphQL Relay Specification, which is the format required when using Relay Modern. The server and the client both have to be built to this specification, which can mean more work if you already have your server built without the specification. The GraphQL Relay Specification consists of three core assumptions:
- A mechanism for refetching an object;
- A description of how to page through connections; and
- Structure around mutations to make them predictable
The QueryRenderer
Fragments in Relay Modern are composed into larger fragments that eventually are composed into a query that should give you all of your information in the tree at once, minimizing the number of requests to the server. The root of all of these fragments in Relay Modern is a component called the QueryRenderer
.
import {
QueryRenderer,
graphql
} from 'react-relay'
import environment from './Environment'
const AppAllPostQuery = graphql`
query AppAllPostQuery {
viewer {
...ListPage_viewer
}
}
`
class App extends Component {
render() {
return (
<QueryRenderer
environment={environment}
query={AppAllPostQuery}
render={({error, props}) => {
if (error) {
return <div>{error.message}</div>
} else if (props) {
return <ListPage viewer={props.viewer} />
}
return <div>Loading</div>
}}
/>
)
}
}
The QueryRenderer takes three props:
- The environment that is a setup file you would create with information about your server, such as the API endpoint and how queries are created into HTTP requests;
- The outermost GraphQL query, containing all other fragments; and
- A render method that determines what to display depending on whether the query is waiting to return, an error occurred, or the data from the query has been returned
Build Step
Relay Modern utilizes a build step using the package relay-compiler
in conjunction with Babel. It takes all of the queries in our app code in addition to our GraphQL schema and outputs runtime artifacts. Because the artifacts contain information about our schema, it can check our queries for errors and use type information from the schema. This means we can potentially write less code after getting the query data back, since we don't have to check for as many errors. This precompilation step is very useful as you can see, but it also can be hard to integrate with some architectures. Here is an example of what we would run to compile our queries:
relay-compiler --src ./src --schema ./schema.graphql
Apollo
Apollo is a client library that has a much lower learning curve than Relay Modern but can be just as powerful. Apollo is much less opinionated than Relay Modern and, because of that, is extremely flexible.
ApolloProvider
As opposed to the colocating of fragments and components that Relay Modern does, in Apollo we can execute queries anywhere. First we must wrap the app in the main ApolloProvider component.
import ApolloClient, { createNetworkInterface } from 'apollo-client'
import { ApolloProvider } from 'react-apollo'
const networkInterface = createNetworkInterface({
uri: '__API_ENDPOINT__'
})
const client = new ApolloClient({
networkInterface
})
class App extends Component {
render() {
return (
<ApolloProvider client={client}>
<ListPage />
</ApolloProvider>
)
}
}
Where in Relay Modern we have to create a fairly large Environment, with Apollo we simply create a network interface with the API endpoint, and pass that to an ApolloClient. The client is setup very quickly in only 6 lines before being passed to the ApolloProvider.
Components
In order to provide a component with data from a GraphQL query in Apollo, we use a Higher Order Component, like with Relay Modern, called graphql
.
import { graphql } from 'react-apollo'
import gql from 'graphql-tag'
const FeedQuery = gql`query allPosts {
allPosts(orderBy: createdAt_DESC) {
id
title
body
}
}`
class ListPage extends Component {
render () {
const { data: { loading, allPosts, refetch }} = this.props
if (loading) {
return (<div>Loading</div>)
}
return (
<div className="container">
<div className="posts">
{allPosts.map((post) =>
<Post key={post.id} post={post} refresh={() => refetch()} />
)}
</div>
</div>
)
}
}
export default graphql(FeedQuery)(ListPage)
ListPage
then will have a prop called data
that contains the list of posts in allPosts
, a boolean called loading
for determining what you want to show, and a refetch
function to rerun the query.
We pass each post contained in allPosts
through a prop into a Post
component without having to write a fragment inside the Post
component, like Relay Modern would require.
Redux
Apollo is built on top of Redux, so it uses Redux as the store for all data coming from back GraphQL queries.
Conclusion
Relay Modern has a very opinionated structure and naming convention that it wants us to follow. This structure can be very awesome such as colocating fragments with components, but it also means that there will be a little more development time associated. We at SmartLogic take pride in well-organized structure so this is a big plus for us in the long run.
The built step in Relay Modern also requires more setup and time but ultimately provides faster queries, better caching, and catching errors before runtime.
Apollo is built on top of Redux, while Relay Modern does not work very well with Redux. This is a plus for Apollo for us because we use Redux in every React project we create currently. Because there is so much structure to Relay Modern's store though, it claims to possibly enable us to do away with Redux and use Relay Modern's store entirely.
Ultimately, I think we at SmartLogic could build React frontends with either Relay Modern or Apollo and be happy with the results. We would prefer the opinionated structure of Relay Modern when possible, but we would also benefit from the flexibility of Apollo and its Redux integration depending on the size of the project.
While Relay Modern may be a more viable option for larger projects where we control all aspects of the backend and frontend, Apollo may be more useful for smaller projects--projects where we have to be more flexible or when we need to use Redux.