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
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:
- We changed the React component to show a list of artworks and include a button to load the next page.
- We changed from
- We added GraphQL fragment variables for
cursorto be passed through to the new
artworksConnection, which we also added.
- Finally, we added a whole new configuration parameter to
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 response for a certain set of variables.
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.