Eli Perkins.

Validating Relay Queries With This One Weird Trick™

tl;dr: Use relay --validate to catch Relay validation errors in CI!

I'm a big fan of GraphQL (seriously, ask me about GraphQL). One tool I've been absolutely loving lately has been Relay. We use Relay at Clubhouse to build some really awesome features in Clubhouse for iOS. As of the writing of this blog post, we've got nearly 100 different components using Relay in some way, shape or form!

Hold up, what's Relay again?

Relay is to GraphQL and data fetching what React is to DOM and UI. It's a declarative way to dictate what data your components need, rather than how and when to fetch it.

A typical use-case of Relay looks something like this. Say you've got an social media app that shows a list of friends for the current user.

Each of these components gets data from a different part of our tree. We can colocate our queries of how to fetch the data for each component using Relay. A common way to do this is using Relay's createFragmentContainer (there's other methods to do this too, to handle pagination, refetching, etc. , but for the sake brevity in this post, let's focus on fragment containers).

Some of the components may look like this:

// in src/Avatar.js
const Avatar = ({ user }) => <Image src={user.thumbnailImgSrc} />;

const AvatarContainer = createFragmentContainer(
  Avatar,
  graphql`
    fragment AvatarContainer_user on User {
      thumbnailImgSrc
    }
  `,
);

// in src/FriendListItem.js
const FriendListItem = ({ user }) => (
  <View>
    <Avatar user={user.thumbnailImgSrc} />
    <Text>{user.name}</Text>
  </View>
);

const FriendListItemContainer = createFragmentContainer(
  FriendListItem,
  graphql`
    fragment FriendListItemContainer_user on User {
      name
      ...AvatarContainer_user
    }
  `,
);

// in src/FriendsList.js
const FriendsList = ({ currentUser }) => (
  <FlatList
    data={currentUser.friends}
    renderItem={({ item }) => <FriendListItem user={item} />}
  />
);

const FriendListItemContainer = createFragmentContainer(
  FriendListItem,
  graphql`
    fragment FriendListItemContainer_currentUser on CurrentUser {
      friends {
        ...FriendListItemContainer_user
      }
    }
  `,
);

Now each of our components composes together both is UI components and it's data-requirements, declaratively!

Dope. Relay seems cool. But what's this __generated__ directory I've got here?

The Relay Compiler will read through our source code to find Relay components and their colocated GraphQL queries to generate artifacts that will be used by the Relay runtime.

These artifacts look something like this (abbreviated here):

// from src/__generated__/AvatarContainer_user.graphql.js
const node /*: ReaderFragment*/ = {
  kind: 'Fragment',
  name: 'AvatarContainer_user',
  type: 'User',
  metadata: null,
  argumentDefinitions: [],
  selections: [
    {
      kind: 'ScalarField',
      alias: null,
      name: 'thumbnailImgSrc',
      args: null,
      storageKey: null,
    },
  ],
};
(node/*: any*/).hash = '693ff4889bc9965ae9f6512d628b7292'; // prettier-ignore
module.exports = node;

We can see that for the Avatar component, the compiler generates a static set of data for the GraphQL fragment it needs to fetch the data.

Relay recommends checking in these artifacts from the Relay Compiler into your source control, as they're crucial and necessary to run your app.

As you work through your app, you'll likely run yarn relay --watch to run the compiler in watch mode to automatically generate these artifacts and as you build new components, pages, features and so on. You'll see that as your change your GraphQL queries, the Relay Compiler will indicate what is changed, and you can see the changes in your git diff as well.

❯ yarn relay
yarn run v1.15.2
$ relay-compiler --src ./src --schema ./schema.graphql --watch

Writing js
Updated:
 - AvatarContainer_user.graphql.js
Unchanged: 2 files

❯ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   src/Avatar.js
	modified:   src/__generated__/AvatarContainer_user.graphql.js

However, these artifacts are only autogenerated if you run the Relay Compiler while you're working! If a team member (or even yourself) forgets to run yarn relay while you're working, and a GraphQL query changes, your generated artifacts will be out of sync with your product code! 😰

🤔 So how do we fix this? How do we prevent our product code's queries from coming out of sync with our autogenerated Relay artifacts?

Luckily, Relay makes this easy.

The Relay Compiler includes a flag called --validate. This flag will run the Relay Compiler and if there are any autogenerated artifacts that will be overwritten based on the GraphQL queries, the compiler will indicate that an artifact is our of date and exit with an error.

❯ yarn relay --validate
yarn run v1.15.2
$ relay-compiler --src ./src --schema ./schema.graphql --validate

Writing js
Out of date:
 - AvatarContainer_user.graphql.js
error Command failed with exit code 101.

This makes it really easy to validate your Relay codebase in CI, just as you might run your tests in CI! Add relay --validate to your CI flow today and catch changes in GraphQL queries before they land on master.


Eli Perkins

Written by Eli Perkins, a mobile engineer based in Denver. Say hello on Mastodon.