Apollo vs Relay Modern: An unbiased look at which GraphQL client to use
Do we even need a client?
First things first — depending on how large your application is, and what you’re trying to achieve with GraphQL, you may not even need a GraphQL client.
If you're just testing the waters with GraphQL and don't want to change your existing app too much, you can just use fetch
in your component like so:
fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ query: '{ hello }' }),}) .then((r) => r.json()) .then((data) => console.log('data returned:', data));
The main benefit to adopting a GraphQL client is the cache implementation. Using fetch is fine to begin with, but you don't want to be using it in an app where users quickly jump between views.
In PerfBeacon we use Apollo to cache query results - which gives us quite a noticable boost in performance. How it works in practice:
You view a list of websites: this list of websites is now cached in memory, for example:
[ { "id": "123", "url": "https://perfbeacon.com" }, { "id": "124", "url": "https://maxrozen.com" }]
You then choose to review reports for your first website. As the GraphQL client already knows the id and url, it doesn't need to refetch this data. It only needs to fetch the array of reports, as shown below:
[ { id: '123', url: 'https://perfbeacon.com' reports: [ { id: '1', date: '2020-01-01', } ] }]
As you now have the list of websites, and a list of reports for the first website, clicking back in your browser to view the list of websites is instant, as no GraphQL request is made.
Essentially, the more the user clicks around your application, the faster your user experience becomes.
Apollo Client
The setup is considerably easier than Relay - it involves installing one package, and adding the ApolloProvider
to the root of your React tree.
The API is nice - they have an equivalent to Relay's QueryRenderer called Query
that does what it says:
<Query query={gql` { rates(currency: "USD") { currency rate } } `}> {({ loading, error, data }) => { if (loading) return <p>Loading...</p>; if (error) return <p>Error :(</p>;
return data.rates.map(({ currency, rate }) => ( <div key={currency}> <p>{`${currency}: ${rate}`}</p> </div> )); }}</Query>
It can be used to manage state in your React app - that is, you can directly write to Apollo's Redux-like store and consume that data in another part of the React tree. Though with React's new Context API, and React's best practices of Lifting State Up you probably won't need it.
import React from 'react';import { ApolloConsumer } from 'react-apollo';
import Link from './Link';
const FilterLink = ({ filter, children }) => ( <ApolloConsumer> {(client) => ( <Link onClick={() => client.writeData({ data: { visibilityFilter: filter } })} > {children} </Link> )} </ApolloConsumer>);
Downsides to Apollo
- It's huge. It weighs in at 10x more than the smallest GraphQL client I'd consider using, and 3x more than urql
Quirks
Apollo is not without quirks however:
- Since Apollo uses
id
to build its cache, forgetting to includeid
in your query can cause some interesting bugs and error messages
On Bundle size
One of the biggest complaints I hear about adopting Apollo is the bundle size (Apollo Boost, the "easiest way to get started with Apollo Client" weighs in at 30.7 kB min+gzipped), so luckily there are also alternative lightweight clients to consider:
- Formidable Labs' urql - 12kB min+gzipped (with perhaps the best README I've ever seen)
- Prisma's graphql-request - 4 kB min+gzipped
- Adam Rackis' micro-graphql-react - 3.1kB min+gzipped
Relay
Setup
The main benefit to using Relay is that relay-compiler
doesn't get included in your frontend bundle, saving your user from downloading the whole GraphQL parser - it "pre-compiles" the GraphQL queries at build time.
What annoys me about Relay is that it requires a fair bit of work to even add to a project. Just to get it running on the client side, you need to:
- add a relay plugin to your
.babelrc
config - set up relay-compiler as a yarn script
- setup a "relay environment" (essentially your own
fetch
utility to pass data to the relay-runtime), and - add
QueryRenderer
components to the React Components you wish to pass your data to
On the server side, you need to:
- Ensure the IDs your app returns are unique across all of your types (meaning you can't return nice ID values like
1, 2, 3
, they need to be liketypename_1, typename_2
)
Developer Experience
The developer experience itself is pretty unpleasant too - relay-compiler
needs to run each time you modify any GraphQL query, or modify the schema. In large frontend teams this means teaching everyone to run relay-compiler
every time you change branches in Git, since almost all of our work involves fetching data from GraphQL in some way.
Update: Thanks to relay-compiler-webpack-plugin you no longer need to remember to re-run the relay-compiler
every time you make a change, webpack will do it for you.
Quirks
Being one of Facebook's Open Source projects doesn't necessarily mean issues get fixed quickly. Occasionally, things break in unexpected ways:
- Using an old version of
graphql
breaksrelay
: https://github.com/facebook/relay/issues/2428 - Errors don't get sent via the error object in GraphQL when using QueryRenderer, instead one needs to create an
error
type, and send the errors through the data object: https://github.com/facebook/relay/issues/1913
In Summary
If I were starting a new project today in the middle of 2020, I’d use Apollo Client by default.
If my users were using cellular data, bandwidth constrained, or using lower-spec devices, I would opt to use urql instead.
I wouldn’t bother rewriting an application from Relay to Apollo. There are benefits, mainly from developer experience/ergonomics of the API, but not enough to outweigh the cost of rewriting. You may also consider writing new features using Apollo, and heavily using code-splitting to avoid sending two GraphQL implementations to your users — I’ve used a similar approach to slowly roll-out TypeScript for companies I’ve worked at.
(Shameless plug for the useEffect book I wrote below)
Tired of infinite re-renders when using useEffect?
A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.
Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.
In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.