In 2017, Artsy adopted Relay in both its front-end web and iOS codebases (using React and React Native, respectively). Generally speaking, this investment has turned out very well for us! Relay empowers product teams to quickly iterate on new features and to share common infrastructure across web and iOS codebases. However, most of the original […]
In 2017, Artsy adopted Relay in both its front-end web and iOS codebases (using React and
React Native, respectively). Generally speaking, this investment has turned out very well for us! Relay empowers
product teams to quickly iterate on new features and to share common infrastructure across web and iOS codebases.
However, most of the original engineers who pioneered using Relay at Artsy have since moved on to their next role;
this has left a knowledge gap where Artsy engineers are comfortable using Relay, but they don’t totally
understand how it works.
This is a problem as old as software engineering itself, and it has a simple solution: learn and then teach others.
We’ll be driving a peer learning group centering around Relay, but today we are going to dive into the part of
Relay that comes up the most in requests for pairing: getting Relay pagination to work. (Note: we’re going to use
plain old Relay and not relay-hooks.)
My goal with this post is to show my thought process when trying to learn about, and clean up our use of, Relay
pagination containers. This post emphasizes the demystifying process and not so much the Relay pagination
containers themselves – we’ll briefly cover some Relay fundamentals before diving into a case study on how
problematic code proliferates through copy-and-paste.
Let’s back up and talk a little bit about what Relay is and how it works. Relay is a framework that glues React
components and GraphQL requests together. React components define the data they need from a GraphQL schema in order
to render themselves, and Relay handles actually fetching GraphQL requests and marshalling data into the React
component tree. It is very efficient because of build-time optimizations by the Relay compiler.
The simplest use of Relay is a fragment container, which is created
from a React component and a GraphQL fragment. (We’re
going to skip over how the GraphQL query is made, but
here are the docs on query renderers if you’re curious.)
1 2 3 4 5 6 7 8 9 10 11 12 13
(At Artsy, we use TypeScript with Relay, but
So we have a plain React component that gets some props, and a Relay fragment container that wraps it, defining the
data that the component needs.
There are other types of Relay containers beyond simple fragment containers.
Refetch containers are like fragment containers except you can
refetch their contents from your GraphQL server (in response to, for example, user interaction). Using a refetch
container is very similar to using a plain fragment container. But today, we want to talk about
pagination containers, which use a GraphQL construct called
connections to show page after page of data.
GraphQL connections are beyond
the scope of this blog post, but they are a way to fetch lists of data without running into the limitations of
returning a simple array. Connections can return metadata about their results, like how many total results there
are, and use cursors (rather than page numbers) for paginating. They also handle when items are inserted or deleted
from the results between requests for pages –
check out this blog post for more
info on how to use connections with Relay.
Pagination containers take considerably more setup than plain fragment containers, and the setup itself is very
fickle. Things simply will not work until you get the configuration exactly correct, and then everything works
perfectly. The setup is largely repeated boilerplate, and what I’ve noticed (from other engineers but also myself)
is that the boilerplate for new pagination containers gets copy-and-pasted from existing ones. We will see how this
leads to small problems getting propagated throughout the codebase, and leads to engineers not feeling confident
when working in pagination containers.
So let’s modify the Relay container above to fetch a list of the artist’s artworks. This is a very simple example,
only used to illustrate how to use pagination containers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
Wow, that’s a lot! I don’t want to get too bogged down in details, so let’s break this apart at a high level:
cursorto be passed through to the new
artworksConnection, which we also added.
This last bit is the part where I see the most frustration. Hopefully what follows will clear things up.
I like to always start by reading the docs. The
is the direction that we paginate through, either
getConnectionFromProps is a
function that returns the GraphQL connection, in case the query has more than one. And
query is used to fetch any
specific page of results.
Those all makes sense to me, but then we arrive at the real gotchas:
docs are helpful, but only if you understand
the internals of how Relay works. Relay has a sophisticated
architecture that delivers some really well-performing code, but its abstractions sometimes
“leak” and you have to deal with underlying implementation
details of Relay (like the Relay store) which you don’t need to know about
most of the time.
So what are these two functions? Let’s return to the docs:
getFragmentVariablesis used when re-rendering the component, to retrieve the previously-fetched GraphQL
getVariablesis used when actually fetching another page, and its return value is given to the
I think of
getFragmentVariables as a kind of caches key for lookup in Relay’s internal store. Our implementation
getFragmentVariables above doesn’t really do anything interesting, but a connection that accepted
filter parameters would need to return those to avoid lookup collisions when the user changed sort and filter
getVariables, which are the variables used for the
query later on. It really ought to be named
getQueryVariables, I think. But I digress.
Every implementation of
getFragmentVariables I could find at Artsy was identical, which makes sense because that
is the default implementation. We shouldn’t be defining this option at all! As far as I can tell, Artsy started
with a few pagination containers that supplied this parameter unnecessarily and it got copy-and-pasted throughout
After revisiting the docs, I noticed other optional parameters that don’t need to be defined either. Let’s rewrite
the call to
createPaginationContainer to only supply the parameters that are required:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
This is a lot nicer! By not specifying unnecessary options, we have a smaller surface area to make mistakes in. We
also have fewer overloaded terms, like “variables”, so now it’s more obvious that
getVariables supplies data for
query below it.
I’ve already sent a pull request to clean up our use of pagination
containers in our React Native app, and will be following up on the web side next. But I wouldn’t have discovered
this if I hadn’t really dug into the docs, which I only did so that I could write this blog post. Earlier I said
that the solution to a knowledge gap is simple: learn, and then teach. I learned a lot about Relay today, and I
hope this blog post illustrates the value in the learn-then-teach approach.