April 19, 2017

Exploring GraphQL Relay – Modern vs. Classic

Sashko Stubailo

Sashko Stubailo

GraphQL is an incredible API technology that lets consumers describe their data requirements with a simple, declarative query language. Even though GraphQL servers are easy to query via a simple HTTP request, GraphQL’s well-specified nature enables us to go beyond just making basic requests. The structure of the schema and queries enables sophisticated client libraries to do data fetching and management for you.

In 2017, Lee ByronYuzhi Zheng, and Joe Savona announced the first release candidate for a rebuild of GraphQL Relay designed to be simpler, faster, and more extensible than ever before. Read their announcement here. It’s a really big deal for the GraphQL community that within the space of a few weeks both of the most popular client libraries — Relay and Apollo Client — are coming out with 1.0 releases.

I’ll give you fair warning: I’m a pretty big GraphQL client nerd. As part of working on Apollo Client, I’ve spent the last year or so thinking about every part of the problem of fetching, caching, and updating GraphQL data. This is going to get pretty in depth, but if you’re looking for extensive discussion and lots of examples about the new Relay release, this post is for you.

We’ll go over the following important topics:

  1. What is Relay Modern and what are its goals?
  2. What are the biggest changes compared to Relay Classic?
  3. How can you try Relay Modern today with a simple example app?

If you want to jump right into code, check out the example on GitHub.

Throughout this post, I’ll refer to versions of Relay prior to 1.0.0, which is being called “Relay Modern”, by the name “Relay Classic”. Special thanks to Joe Savona for looking over this post before it went out!

What is GraphQL Relay?

Relay is a GraphQL client framework. In general, a GraphQL client framework or library enables developers to eliminate data fetching boilerplate, and declaratively pass data from a GraphQL API into UI components. In a React application, it would live in a similar place to something like Redux or MobX, and can often replace those systems entirely. Relay also has several direct competitors in the frontend GraphQL fetching space, including our own Apollo Client library for React. These share many of the same goals and features but make different tradeoffs.

Modularizing data fetching

The main premise of Relay is that your data fetching should be just as modular as your UI. This is accomplished by encouraging fine-grained use of GraphQL fragments. Relay is designed to enable developers to put each React component together with a small GraphQL fragment in the same file, which makes it very easy to ensure that you always fetch exactly the data you need for that component. These fragments can be composed into larger fragments and finally queries, to get all of the data required for a particular view with the minimal number of requests to the server.

One React component and a Relay container wrapping it with a fragment, from the Facebook announcement post.

Leveraging a build step

One way that Relay has always stood out in the JavaScript GraphQL client ecosystem is that it uses a build step to precompile queries. Originally with babel-plugin-relay, and now also with the relay-compiler, Relay adds a step to your build process which takes in all of the queries in the app and your GraphQL schema, and outputs runtime artifacts with additional data and helpers. Because this build process contains schema knowledge, it can also check your queries for errors and inject type information, which enables the framework to do more under the hood to optimize data fetching and reduce the amount of code to be written by the application developer.

Relay Modern takes this one step further. While Relay Classic only processed one query at a time, adding some useful metadata and converting to a runtime format, the new relay-compiler works on all of the queries in your app at once, enabling it to do a larger set of transformations. See the complete list in the source code.

One thing to note is that while this opinionated approach has benefits for performance and developer ergonomics, requiring queries to be precompiled makes it difficult or impossible to integrate into some architectures. Taking this tradeoff of requiring a specific build step is one of the primary differentiators of Relay.

What is different in Relay Modern vs. Relay Classic?

Relay Modern is a redesign of the core and API of Relay to achieve the same main principles of declarative fetching and modular data requirements, while trimming some of the features that made the original implementation more complex and harder to optimize. One nice thing is that even though there are a lot of changes, there’s a handy compatibility mode to help you migrate incrementally.

So what are the biggest changes from Relay Classic?

We realized that a simpler approach could be easier to understand and also allow for greater performance. — Relay Modern announcement post

Static and persist-able queries

Several features of Relay Classic relied heavily on runtime query manipulation, and the particular syntax of the containers and fragment composition didn’t lend itself well to static analysis. For the full details of why this matters, check out my blog post about all of the benefits of having statically-analyzable GraphQL queries. At a high level there are three main benefits to avoiding dynamic query generation:

  1. Validation. The more you can check your queries for correctness at development time, the earlier you catch typos and other problems. Some types of GraphQL code are harder to check than others, as we’ll see below.
  2. Persisting. It’s useful to be able to analyze an application and generate a list of all possible queries it might send to the server — in the GraphQL community, this is referred to as “persisted queries”.
  3. Build-time transformation and optimization. relay-compiler will make some changes to the queries you write, to inline constant values and remove redundant fields. Read more in the Relay docs.

Here’s an example of two Relay Classic fragment containers, one of which uses a fragment from the other:

// Some component that defines a fragment
Greeting = Relay.createContainer(Greeting, {
  fragments: {
    greetings: () => Relay.QL`
      fragment on Greetings {
        hello
      }
    `,
  },
});

// Some other component that uses the fragment from above
App = Relay.createContainer(App, {
  fragments: {
    greetings: () => Relay.QL`
      fragment on Greetings {
        username
        ${Greeting.getFragment('greetings')},
      }
    `,
  },
});

If you’re a GraphQL build tool, there’s a problem here — if you look at the Relay.QL statement from the second component, it just says Greeting.getFragment('greetings'). Let’s say you wanted to validate that the fragment is being used in the correct place. You would have to understand JS syntax to track down the Greeting component, look inside its fragments dictionary, and extract the type condition on the fragment. If the component was in a different file, you’d also have to figure out the imports and get the right file. It’s definitely not impossible, but pretty hard.

To improve the situation and make it easy to develop a tool to accomplish the above goal, Relay Modern doesn’t use interpolation style syntax for fragments. Instead, it looks like this:

// Some component that defines a fragment
Greeting = createFragmentContainer(Greeting, graphql`
  fragment Greeting_greeting on Greetings {
    hello
  }
`);

// Some other component that uses the fragment from above
App = createFragmentContainer(App, graphql`
  fragment App_greetings on Greetings {
    username
    ...Greeting_greeting
  }
`);

You can see how this is much easier to work with from a build tooling perspective. All you need to do is pick out the template literals marked off by graphql, throw them all together, and run normal GraphQL validation.

All native mobile GraphQL clients, both those used internally at Facebook and ones written by community, work this way, and it’s good to see the JS community move in the same direction. I’m also excited that this way of declaring fragments is similar to the approach we’ve been encouraging in Apollo Client, where we also suggest unique fragment names to enable better tooling.

Simpler mutations, no more fat queries

When people talk about GraphQL, they usually start with the queries. Building a UI with read-only data using GraphQL is an absolute dream, since you can super cleanly declare the data you need and it shows up almost magically. But mutations are a bit more complex, because the client needs to do two extra things:

  1. Mutation result selection. In GraphQL, mutations just return a reference to a specific GraphQL type. It’s up to the client library or app developer to figure out what might have changed and what needs to be refetched, and provide the right selection set for the result.
  2. Update store. To ensure consistency across queries, all of today’s most popular GraphQL client libraries operate on a relatively similar normalized cache model. Once the server returns the new data, someone has to specify how to incorporate that new information into the GraphQL store on the client so that other queries can be updated. Unfortunately, GraphQL servers don’t provide much help in this regard.

While working on Apollo Client over the last year, we’ve spent the overwhelming majority of our time working with the community to figure out how to make mutations nice in the general case, and let me tell you — it’s no walk in the park. This turned out to be the case in Relay Classic as well. Here’s how the two points above worked before Relay Modern:

  1. Mutation result selection with fat queries and tracked queries. There are two main factors you need to consider when deciding what to refetch after a GraphQL mutation: (1) the data that might have changed on the server as a result of the mutation, and (2) the data currently being displayed in the UI. In Relay Classic, the data that might have changed on the server was specified by the client-side application developer in a construct called a “fat query” — so called because it had to describe the entire set of possible affected data, and was often quite large. The other part, the data being displayed in the UI, was actually tracked by Relay Classic internally with a feature aptly named “tracked queries”. So when a mutation was actually sent to the server, Relay Classic would take the fat query and intersect it with the tracked queries, hopefully refetching exactly the right amount of data to update the UI.
  2. Store updates via configuration actions. After the above process determined what to actually retrieve from the server, there was a set of allowed operations of what you could do with the result as a client developer. These looked a lot like Flux actions, and were perhaps the most Flux-ey part of Relay Classic. The configs you could choose from were: FIELDS_CHANGENODE_DELETERANGE_ADD, andRANGE_DELETE. These needed to be decorated with what particular nodes or ranges were to be affected.

Note: There was also an API called <em>GraphQLMutation</em> that allowed skipping part (1) above, and directly specifying the data to be refetched without the fat queries.

OK, so that description was quite a mouthful. You can see a complete mutation example in the Relay Classic guide. The intentions of the Relay Classic mutation API were in the right place: It was designed to make mutations as reusable and declarative as possible. You didn’t have to think about the different parts of your app when writing a fat query — you could just select as much data as you want and Relay would filter it down for you. The configs felt declarative, because they allowed you to operate on the level of adding and removing result objects.

However, armed with our understanding of the benefits of static GraphQL queries from above, we can see the same issues here. The mutation query was dynamically generated, meaning the server couldn’t predict what would be selected on the result. And the mutation configs could often end up feeling less declarative and more like a small window through which you had to tell Relay everything you wanted to happen to the store.

Thankfully, mutations in Relay Modern are much simpler, closer to “regular” GraphQL mutations, and more powerful. Just check it out:

const mutation = graphql`
  mutation MarkReadNotificationMutation(
    $input: MarkReadNotificationData!
  ) {
    markReadNotification(data: $input) {
      notification {
        seenState
      }
    }
  }
`;

const variables = {
  input: {
    source,
    storyID,
  },
};

commitMutation(
  environment,
  config: {
    mutation,
    variables,
    updater: (store) => {
      // Not necessary for simple mutations.
      // You can use this to do things like:
      // store.get('asdf').setValue('newValue', 'fieldName');
    },
  },
);

OK, so what are the answers to our questions above?

  1. Just write the mutation selection. Like in the newer Relay Classic mutation API, you just write the data you want to refetch. Pretty straightforward.
  2. Automatic updates, or imperative updates via store proxy. When specifying how the store needs to change once the mutation result comes back, we no longer need to fit our intentions inside a small set of available configs. We now get access to a fully-powered store proxy object, which lets us just do whatever we want. Great!

If it sounds much simpler, that’s because it is!

As an aside, I’m really happy that there is a lot of convergence here — the API is quite similar to the most recent mutation API for Apollo Client. That’s because we didn’t come up with it in a vacuum. Everyone in the GraphQL community, including the team at Facebook and our team at Apollo, is talking all the time and sharing these ideas. We were intrigued by a presentation by Greg Hurrell from Facebook, and called out the genesis of the idea in our announcement post for the new feature.

View-agnostic runtime

One really neat feature of the new core is that relay-runtime and relay-compiler are now decoupled from the UI integration, which lives in react-relay. This means you can now more easily build your own integrations, or use Relay with other view technologies, like Angular, Vue.js, Ember, and others! The Relay team intends these components to stay decoupled, and enable their reuse in other toolsets and libraries.

Replacing setVariables with more targeted APIs

A final note, before we move on, let’s pour one out for setVariables. The new Relay Modern docs have a guide for how to migrate away from it. Essentially, setVariables was somewhat of a catch-all API for changing the data requirements on a fragment after the initial page load, and handled aspects as diverse as pagination, fetching more data, setting arguments, and more. It could also sometimes result in hard-to-analyze query manipulations. In Relay Modern, the recommended approach is to use the right kind of container for your data needs, by mixing and matching QueryRendererFragmentContainerRefetchContainer, and PaginationContainer.

Additional new features

Those are the changes I think will affect current Relay developers the most. The “what’s new in Relay Modern” page has a more complete list of changes and improvements. Not all of them are fully documented yet, but I look forward to trying them out as soon as I can:

  • Dramatically smaller bundle size
  • Garbage collection to evict old data from the store
  • GraphQL subscriptions support (if you’d like to wire Relay up with the websocket GraphQL subscriptions transport, which supports a variety of servers including Node, Graphcool, and Scaphold, let’s talk!)
  • Custom field handlers for more fine-grained control of store format
  • Client schema extensions to allow client-only data to be kept in the Relay store
  • And one that’s been drumming up a lot of early excitement in the community: Flow type generation from the compiler!

I’m really excited to see how people use all of these new features.

Try running a simple example app

No post about a new library is complete without a small example you can download and run. So let’s do just that! Without further ado, here’s a very small app you can download and run to have Relay Modern working right away:apollographql/relay-modern-hello-worldrelay-modern-hello-world – An example app that fetches and displays one query with React and Relay Moderngithub.com

It’s not much to look at, it just displays a basic list from the new repositories feed on GitHunt, our example app server:

If you’ve gotten this far, definitely clone the code and give it a run. You can get all of the details in the repository README, but here’s a quick rundown of the different parts:

  1. Some build tooling to set up the static query building I was talking about above, including a schema file, babel plugin, and compiler script.
  2. The generated query output, which looks like a preprocessed GraphQL AST with some extra information about types and cache keys.
  3. The setup code to initialize a Relay Environment and Network to talk to the server.
  4. The application code to fetch the query and pass the results into a React component. Here’s what that part looks like:
<QueryRenderer
  environment={environment}

  query={graphql`
    query AppFeedQuery {
      feed (type: NEW, limit: 5) {
        repository {
          owner { login }
          name
          stargazers_count
        }
        postedBy { login }
      }
    }
  `}

  render={({error, props}) => {
    if (error) {
      return <div>{error.message}</div>;
    } else if (props) {
      console.log(props.feed);
      return <Feed feed={props.feed} />;
    }
    return <div>Loading</div>;
  }}
/>

Looks great!

If you want to see what a similar query looks like with Apollo Client to compare, check out the similar example on our home page.

Conclusion

Relay was announced right alongside GraphQL at React Conf in February of 2015, and the initial version was publicly released in August of 2015, a little while after the reference GraphQL implementation. In the meanwhile, a lot of exciting stuff has been going on in the GraphQL community. Now, almost a year and a half later, Relay Modern is here — a reinterpretation of the same principles with a simplified interface, better performance, and more.

I couldn’t be more excited about all of this great stuff happening in the GraphQL community. On the Apollo team, we have one main goal: Facilitate the adoption of GraphQL in the best way we can so that more people can benefit from it. Having several really awesome options in the JavaScript GraphQL client space, with different tradeoffs and benefits, is the best possible place to be. It’s the sign of a healthy ecosystem that people are racing to build the best possible tools. There are dozens of GraphQL server implementations in every conceivable language, and I hope to one day see the same kind of diversity on the client.

This post is primarily made up of first impressions from trying out the library and reading the documentation, and I can’t wait to learn more, share ideas, and collaborate on cool stuff. If you’re excited about Relay Modern, I encourage you to provide feedback about the RC and even contribute some fixes!

If you like reading about GraphQL, follow the Apollo team, and stay tuned for our upcoming posts with more details about Relay and thoughts about the future roadmap for Apollo Client!

Written by

Sashko Stubailo

Sashko Stubailo

Read more by Sashko Stubailo