Moving from Redux/GraphQL to Relay

TLDR; React/GraphQL is designed for small to medium sized apps with simple mutations while Relay can more simply handle more complex mutations but requires more work on the server-side.

My Experience with Redux/GraphQL

On my last Redux project, I decided to use GraphQL instead of REST. I loved it. As my data needs changed, only my data fetchers needed to be updated, no backend code (unless it was a new field). I didn’t have to scrounge around the backend or consult some crazy ass swagger docs to figure out what the response was going to be. I built my graphql queries using Graphiql, an autocompleting editor (automatically mounted on /graphql) and could immediately see what the JSON response would be as I wrote queries.

What was the overhead of adding GraphQL? A 15 line`express-graphql` config block in my server.js and a schema file sitting at around ~700 LOC. I refactored from 4 REST endpoints to this in a 1/2 day, with no previous GraphQL experience.

Maintainability? Adding authentication was as easy as adding a token to the GraphQL context and a `requireAuth(‘AdminRole’, …)` around the resolver function. When I found a state machine “hidden” in one object, I “split” the table up into multiple types (using the state field as a discriminator). Each new type had just the fields available to it at the particular state it was in. I couldn’t possibly query a field that didn’t exist or GraphiQL would yell at me. This would have been way harder to do with pure REST, and stupid hard to document/communicate.

[ Only thing I couldn’t do? I couldn’t find a way to validate my GraphQL queries at compile time. This would have enabled me to not have to write brittle tests for my fetchers before refactoring. There were lots of projects that did this for GraphQL ‘flavors’ but I couldn’t get it working in the hour I set aside to do so. Please comment if you have gotten this working for vanilla GraphQL template strings. ]

What is Relay?

Now that I understood GraphQL, I wanted to understand what relay brought to the table compared to redux. Relay’s website says it’s principles are “declarative, colocation, and mutation,” but what does that mean? TLDR; React Components declare what their data needs are, Relay caches everything it can and you HAVE to tell Relay exactly what changes when you mutate things.

What do they mean by colocation? Well, you don’t need to define fetchers anymore. No more handwritten graphql queries for me! But at what cost?

Defining the Effects of a Mutation

The largest cost of using Relay is adding an exhaustive description about the effects of your mutation queries. If you don’t change anything, it’s free! But we don’t do that. And often one mutation with set off a chain of other mutations (such as liking a post increases the count).

In my Redux/GraphQL application, I didn’t really worry about mutation. On the server-side, I was “lazy” (aka efficient) with my Redux/GraphQL mutations, returning `true` or `false` on success or failure respectively. If a mutation affected the current state, I handled it via reducers/recompose (optimistically increase the count when the `ADD_FRIEND` action is fired, update it with the server value on `ADD_FRIEND_SUCCESS` and reset it back on `ADD_FRIEND_ERROR`). I’d refetch entire swaths of data when I loaded a new screen rather than attempting some “crazy” caching scheme. Basically, I kept mutation side effects a front-end concern and kept the backend as simple as possible.

With Relay, the client side caches every entity (well, just the fields it has fetched for it) and only refetches an entity if it hasn’t heard of that entity before (or is missing fields). On the server-side, every mutation must return a FatQuery that lists out what is now invalid in addition to returning the result of the query. This is a LOT more work as you have to explicitly state, in every mutation, what it could change. It does, however, relieve you from writing optimistic code around mutation side-effects, which could get out of hand in certain situations in Redux/GraphQL land.

The Viewer Query Pattern

Relay also changes how you optimally define your queries. Most Relay projects use something called the viewer query pattern.

In my Redux/GraphQL app, I wrote a bunch of top-level queries, such as `recentPosts(lastFetched:${lastFetchedTime})` and `userPosts`. This doesn’t work well with Relay. The idea of the viewer pattern is to register every type under a single viewer type and only define one top-level query: the viewer. Almost all queries just modify a subselection of the viewer object; The viewer is only completely invalidated if the SERVER tells it to (by changing the id), which is a rare and apocalyptic event. This pattern also enables Relay to easily invalidate any single node by type/id. Overall, a must!

I am not sure at all what the viewer pattern means for authentication, but it would likely be more secure; you’d be forced to restrict a resource instead of a query (which may accidentally access a restricted resource or provide a backdoor around a rate-limit).

Large Initial Request, Small Updates

Additionally, Relay makes really large initial GraphQL requests that heavily utilize fragments. For every component with a Relay GraphQL block, Relay will generate a new fragment. For an initial request, you might get something like this: At first this set off a ton of red flags. This looked as bloated as REST, but on the request side AND the response side. Then I realized, each fragment was a component. Sure, the first query might be this large, but subsequent queries will be very, very small. You may also notice the initial response will be bloated as well, and full of duplicate data. Why? Well, Relay could cache/render in parallel (I have no idea if it does). In short, Relay optimizes for updates and render time. This makes a ton of sense for me in an isomorphic world, assuming the Relay cache is hydrated on the client side (I have no evidence for/against this).

Relay vs. Redux/GraphQL

Redux/GraphQL is simpler in situations where:

  • Queries are few and can be separate from view code
  • You don’t need client side caching or don’t mind the network overhead or refetching data.
  • Mutations affecting one type don’t affect other types.

Relay is simpler when:

  • Views should know what data they need
  • Mutations of one type affect other types.
  • You are optimizing network requests for update and don’t mind a large initial request.

Things I didn’t look into

I didn’t look much into Subscriptions, which Relay likely has easier support for (as they were added to GraphQL to support a need in Relay).

I also haven’t dived deep into graphql-relay which should simplify writing a graphql backend for a relay app.

I have minimal experience with both GraphQL and Relay. Thanks for commenting about things I misunderstood/missed!

Like what you read? Give Don Abrams a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.