DEV Community

loading...

Relay: the GraphQL client that wants to do the dirty work for you

zth profile image Gabriel Nordeborn ・21 min read

This series of articles is written by Gabriel Nordeborn and Sean Grove. Gabriel is a frontend developer and partner at the Swedish IT consultancy Arizon and has been using Relay for a long time. Sean is a co-founder of OneGraph.com, unifying 3rd-party APIs with GraphQL.

This is a series of articles that will dive juuuuust deep enough into Relay to answer - definitively - one question:

Why in the world would I care about Relay, Facebook’s JavaScript client framework for building applications using GraphQL?

It’s a good question, no doubt. In order to answer it, we’ll take you through parts of building a simple page rendering a blog. When building the page, we’ll see two main themes emerge:

  1. Relay is, in fact, an utter workhorse that wants to do the dirty work for you.
  2. If you follow the conventions Relay lays out, Relay will give you back a fantastic developer experience for building client side applications using GraphQL.

We’ll also show you that Relay applications are scalable, performant, modular, and resilient to change by default, and apps built with it are future proofed for the new features in development for React right now.

Relay comes with a (relatively minor) set of costs, which we’ll examine honestly and up-front, so the tradeoffs are well understood.

Setting the stage

This article is intended to showcase the ideas and philosophy of Relay. While we occasionally contrast how Relay does things against other GraphQL frameworks, this article is not primarily intended as a comparison of Relay and other frameworks. We want to talk about and dive deep into Relay all by itself, explain its philosophy and the concepts involved in building applications with it.

This also means that the code samples in this article (there are a few!) are only here to illustrate how Relay works, meaning they can be a bit shallow and simplified at times.

We’ll also focus exclusively on the new hooks-based APIs for Relay, which come fully-ready for React’s Suspense and Concurrent Mode. While the new APIs are still marked as experimental, Facebook is rebuilding facebook.com using Relay and said APIs exclusively for the data layer.

Also, before we start - this article will assume basic familiarity with GraphQL and building client side JavaScript applications. Here’s an excellent introduction to GraphQL if you feel you’re not quite up to speed. Code samples will be in TypeScript, so a basic understanding of that will help too.

Finally, this article is pretty long. See this as a reference article you can come back to over time.

With all the disclaimers out of the way, let’s get going!

Quick overview of Relay

Relay is made up of a compiler that optimizes your GraphQL code, and a library you use with React.

Before we dive into the deep end of the pool, let’s start with a quick overview of Relay. Relay can be divided into two parts:

  1. The compiler: responsible for all sorts of optimizations, type generation, and enabling the great developer experience. You keep it running in the background as you develop.
  2. The library: the core of Relay, and bindings to use Relay with React.

At this point, all you need to know about the compiler is that it’s a separate process you start that watches and compiles all of your GraphQL operations. You'll hear more about it soon though.

In addition to this, for Relay to work optimally, it wants your schema to follow three conventions:

  • All id fields on types should be globally unique (i.e. no two objects - even two different kinds of objects - may share the same id value).
  • The Node interface, meaning: objects in the graph should be fetchable via their id field using a top level node field. Read more about globally unique id’s and the Node interface (and why it’s nice!) here.
  • Pagination should follow the connection based pagination standard. Read more about what connection based pagination is and why it is a good idea in this article.

We won’t dive into the conventions any deeper at this point, but you’re encouraged to check out the articles linked above if you’re interested.

At the heart of Relay: the fragment

Let’s first talk about a concept that's at the core of how Relay integrates with GraphQL: Fragments. It’s one of the main keys to Relay (and GraphQL!)'s powers, after all.

Simply put, fragments in GraphQL are a way to group together common selections on a specific GraphQL type. Here’s an example:

    fragment Avatar_user on User {
      avatarUrl
      firstName
      lastName
    }

For the curious: Naming the fragment Avatar_user is a convention that Relay enforces. Relay wants all fragment names to be globally unique, and to follow the structure of <moduleName>_<propertyName>. You can read more about naming conventions for fragments here, and we'll talk about why this is useful soon.

This defines a fragment called Avatar_user that can be used with the GraphQL type User. The fragment selects what’s typically needed to render an avatar. You can then re-use that fragment throughout your queries instead of explicitly selecting all fields needed for rendering the avatar at each place where you need them:

    # Instead of doing this when you want to render the avatar for the author 
    # and the first two who liked the blog post...
    query BlogPostQuery($blogPostId: ID!) {
      blogPostById(id: $blogPostId) {
        author {
          firstName
          lastName
          avatarUrl
        }
        likedBy(first: 2) {
          edges {
            node {
              firstName
              lastName
              avatarUrl
            }
          }
        }
      }
    }

    # ...you can do this
    query BlogPostQuery($blogPostId: ID!) {
      blogPostById(id: $blogPostId) {
        author {
          ...Avatar_user
        }
        likedBy(first: 2) {
          edges {
            node {
              ...Avatar_user
            }
          }
        }
      }
    }

This is convenient because it allows reusing the definition, but more importantly it lets you add and remove fields that are needed to render your avatar as your application evolves in a single place.

Fragments allow you to define reusable selections of fields on GraphQL types.

Relay doubles down on fragments

To scale a GraphQL client application over time, it’s a good practice to try and co-locate your data requirements with the components that render said data. This will make maintenance and extending your components much easier, as reasoning about your component and what data it uses is done in a single place.

Since GraphQL fragments allow you to define sub-selections of fields on specific GraphQL types (as outlined above), they fit the co-location idea perfectly.

So, a great practice is to define one or more fragments describing the data your component needs to render. This means that a component can say, “I depend on these 3 field from the User type, regardless of who my parent component is.” In the example above, there would be a component called <Avatar /> that would show an avatar using the fields defined in the Avatar_user fragment.

Now, most frameworks let you use GraphQL fragments one way or another. But Relay takes this further. In Relay, almost everything revolves around fragments.

How Relay supercharges the GraphQL fragment

At its core, Relay wants every component to have a complete, explicit list of all of its data requirements listed alongside the component itself. This allows Relay to integrate deeply with fragments. Let’s break down what this means, and what it enables.

Co-located data requirements and modularity

With Relay, you use fragments to put the component’s data requirements right next to the code that’s actually using it. Following Relay's conventions guarantees that every component explicitly lists every field it needs access to. This means that no component will depend on data it doesn't explicitly ask for, making components modular, self-contained and resilient in the face of reuse and refactoring.

Relay does a bunch of additional things to enable modularity through using fragments too, which we'll visit a bit later in this article.

Performance

In Relay, components will only re-render when the exact fields they're using change - with no work on your part! This is because each fragment will subscribe to updates only for the data it selects.

That lets Relay optimize how your view is updated by default, ensuring that performance isn’t unnecessary degraded as your app grows. This is quite different to how other GraphQL clients operate. Don’t worry if that didn’t make much sense yet, we’ll show off some great examples of this below and how important it is for scalability.

With all that in mind, let’s start building our page!

Relay doubles down on the concept of fragments, and uses them to enable co-location of data requirements, modularity and great performance.

Building the page to render the blog post

Here’s a wireframe of what our page showing a single blog post will look like:

First, let’s think of how we’d approach this with getting all the data for this view through a single top-level query. A very reasonable query to fulfill the wireframe’s need might look something like this:

    query BlogPostQuery($blogPostId: ID!) {
      blogPostById(id: $blogPostId) {
        author {
          firstName
          lastName
          avatarUrl
          shortBio
        }
        title
        coverImgUrl
        createdAt
        tags {
          slug
          shortName
        }
        body
        likedByMe
        likedBy(first: 2) {
          totalCount
          edges {
            node {
              firstName
              lastName
              avatarUrl
            }
          }
        }
      }
    }

One query to fetch all the data we need! Nice!

And, in turn, the structure of UI components might look something like this:

    <BlogPost>
      <BlogPostHeader>
        <BlogPostAuthor>
          <Avatar />
        </BlogPostAuthor>
      </BlogPostHeader>
      <BlogPostBody>
        <BlogPostTitle />
        <BlogPostMeta>
          <CreatedAtDisplayer />
          <TagsDisplayer />
        </BlogPostMeta>
        <BlogPostContent />
        <LikeButton>
          <LikedByDisplayer />
        </LikeButton>
      </BlogPostBody>
    </BlogPost>

Let’s have a look at how we’d build this in Relay.

Querying for data in Relay

In Relay, the root component rendering the blog post would typically look something like this:

    // BlogPost.ts
    import * as React from "react";
    import { useLazyLoadQuery } from "react-relay/hooks";
    import { graphql } from "react-relay";
    import { BlogPostQuery } from "./__generated__/BlogPostQuery.graphql";
    import { BlogPostHeader } from "./BlogPostHeader";
    import { BlogPostBody } from "./BlogPostBody";

    interface Props {
      blogPostId: string;
    }

    export const BlogPost = ({ blogPostId }: Props) => {
      const { blogPostById } = useLazyLoadQuery<BlogPostQuery>(
        graphql`
          query BlogPostQuery($blogPostId: ID!) {
            blogPostById(id: $blogPostId) {
              ...BlogPostHeader_blogPost
              ...BlogPostBody_blogPost
            }
          }
        `,
        {
          variables: { blogPostId }
        }
      );

      if (!blogPostById) {
        return null;
      }

      return (
        <div>
          <BlogPostHeader blogPost={blogPostById} />
          <BlogPostBody blogPost={blogPostById} />
        </div>
      );
    };

Let’s break down what’s going on here, step by step.

      const { blogPostById } = useLazyLoadQuery<BlogPostQuery>(
        graphql`
          query BlogPostQuery($blogPostId: ID!) {
            blogPostById(id: $blogPostId) {
              ...BlogPostHeader_blogPost
              ...BlogPostBody_blogPost
            }
          }
        `,
        {
          variables: { blogPostId }
        }
      );

The first thing to note is the React hook useLazyLoadQuery from Relay:
const { blogPostById } = useLazyLoadQuery<BlogPostQuery>. useLazyLoadQuery will start fetching BlogPostQuery as soon as the component renders.

For type safety, we’re annotating useLazyLoadQuery to explicitly state the type, BlogPostQuery, which we import from ./__generated__/BlogPostQuery.graphql. That file is automatically generated (and kept in sync with changes to the query definition) by the Relay compiler, and has all the type information needed for the query - how the data coming back looks, and what variables the query wants.

Disclaimer time!: As mentioned, useLazyLoadQuery will start fetching the query as soon as it renders. However, note that Relay actually doesn’t want you to lazily fetch data on render like this. Rather, Relay wants you to start loading your queries as soon as you can, like right when the user is clicking the link to a new page, instead of as the page renders. Why this is so important is talked about at length in this blog post, and in this talk, which we warmly recommend you to read and watch.
We’re still using the lazy load variant in this article though because it's a more familiar mental model for most people, and to keep things as simple and easy to follow as possible. But, please do note that as mentioned above this isn't how you should fetch your query data when building for real with Relay.

Next, we have our actual query:

    graphql`
      query BlogPostQuery($blogPostId: ID!) {
        blogPostById(id: $blogPostId) {
          ...BlogPostHeader_blogPost
          ...BlogPostBody_blogPost
      }
    }`

Defining our query, there’s really not a whole lot left of the example query we demonstrated above. Other than selecting a blog post by its id, there’s only two more selections - the fragments for <BlogPostHeader /> and <BlogPostBody /> on BlogPost.

Notice that we don’t have to import the fragments we’re using. These are included automatically by the Relay compiler. More about that in a second.

Building your query by composing fragments together like this is very important. Another approach would be to let components define their own queries and be fully responsible for fetching their own data. While there are a few valid use cases for this, this comes with two major problems:

  • A ton of queries are sent to your server instead of just one.
  • Each component making their own query would need to wait until they’re actually rendered to start fetching their data. This means your view will likely load quite a lot slower than needed, as requests would probably be made in a waterfall.

In Relay, we build UIs by composing components together. These components define what data they need themselves in an opaque way.

How Relay enforces modularity

Here’s the mental model to keep in mind with the code above:

As the BlogPost component, I only know I want to render two children components, BlogPostHeader and BlogPostBody. I don’t know what data they need (why would I? That’s their responsibility to know!).
Instead, they’ve told me all the data they need is in a fragment called BlogPostHeader_blogPost and BlogPostBody_blogPost on the BlogPost GraphQL type. As long as I include their fragments in my query, I know I’m guaranteed to get the data they need, even though I don’t know any of the specifics. And when I have the data they need, I'm allowed to render them.

We build our UI by composing components that define their own data requirements in isolation. These components can then be composed together with other components with their own data requirements. However, no component really knows anything about what data other components need, other than from what GraphQL source (type) the component needs data. Relay takes care of the dirty work, making sure the right component gets the right data, and that all data needed is selected in the query that gets sent to the server.

This allows you, the developer, to think in terms of components and fragments in isolation, while Relay handles all the plumbing for you.

Moving on!

The Relay compiler knows all GraphQL code you’ve defined in your project

Notice that while the query is referencing two fragments, there’s no need to tell it where or in what file those fragments are defined, or to import them manually to the query. This is because Relay enforces globally unique names for every fragment, so that the Relay compiler can automatically include the fragment definitions in any query that's being sent to the server.

Referencing fragment definitions by hand, another inconvenient, manual, potentially error-prone step, is no longer the developer’s responsibility with Relay.

Using fragments tightly coupled to components allows Relay to hide the data requirements of a component from the outside world, which leads to great modularity and safe refactoring.

Finally, we get to rendering our results:

      // Because we spread both fragments on this object
      // it's guaranteed to satisfy both `BlogPostHeader`
      // and `BlogPostBody` components.
      if (!blogPostById) {
        return null;
      }

      return (
        <div>
          <BlogPostHeader blogPost={blogPostById} />
          <BlogPostBody blogPost={blogPostById} />
        </div>
      );

Here we render <BlogPostHeader /> and <BlogPostBody />. Looking carefully, you may see that we render both by passing them the blogPostById object. This is the object in the query where we spread their fragments. This is the way fragment data is transferred with Relay - passing the object where the fragment has been spread to the component using the fragment, which the component then uses to get the actual fragment data. Don't worry, Relay doesn't leave you hanging. Through the type system Relay will ensure that you're passing the right object with the right fragment spread on it. More on this in a bit.

Whew, that’s a few new things right there! But we’ve already seen and expanded on a number of things Relay does to help us - things that we would normally have to do manually for no additional gain.

Following Relay’s conventions ensures that a component cannot be renderer without it having the data it asks for. This means you’ll have a hard time shipping broken code to production.

Let’s continue moving down the tree of components.

Building a component using fragments

Here's the code for <BlogPostHeader />:

    // BlogPostHeader.ts
    import * as React from "react";
    import { useFragment } from "react-relay/hooks";
    import { graphql } from "react-relay";
    import {
      BlogPostHeader_blogPost$key,
      BlogPostHeader_blogPost
    } from "./__generated__/BlogPostHeader_blogPost.graphql";
    import { BlogPostAuthor } from "./BlogPostAuthor";
    import { BlogPostLikeControls } from "./BlogPostLikeControls";

    interface Props {
      blogPost: BlogPostHeader_blogPost$key;
    }

    export const BlogPostHeader = ({ blogPost }: Props) => {
      const blogPostData = useFragment<BlogPostHeader_blogPost>(
        graphql`
          fragment BlogPostHeader_blogPost on BlogPost {
            title
            coverImgUrl
            ...BlogPostAuthor_blogPost
            ...BlogPostLikeControls_blogPost
          }
        `,
        blogPost
      );

      return (
        <div>
          <img src={blogPostData.coverImgUrl} />
          <h1>{blogPostData.title}</h1>
          <BlogPostAuthor blogPost={blogPostData} />
          <BlogPostLikeControls blogPost={blogPostData} />
        </div>
      );
    };

Our examples here only define one fragment per component, but a component could define any number of fragments, on any number of GraphQL types, including multiple fragments on the same type.

Let’s break it down.

    import {
      BlogPostHeader_blogPost$key,
      BlogPostHeader_blogPost
    } from "./__generated__/BlogPostHeader_blogPost.graphql";

We import two type definitions from the file BlogPostHeader_blogPost.graphql, autogenerated by the Relay compiler for us.

The Relay compiler will extract the GraphQL fragment code from this file and generate type definitions from it. In fact, it will do that for all the GraphQL code you write in your project and use with Relay - queries, mutations, subscriptions and fragments. This also means that the types will be kept in sync with any change to the fragment definition automatically by the compiler.

BlogPostHeader_blogPost contains the type definitions for the fragment, and we pass that to useFragment (useFragment which we'll talk more about soon) ensuring that interaction with the data from the fragment is type safe.

But what on earth is BlogPostHeader_blogPost$key on line 12 in interface Props { … }?! Well, it has to do with the type safety. You really really don’t have to worry about this right now, but for the curious we’ll break it down anyway (the rest of you can just skip to the next heading):

That type definition ensures, via some dark type magic, that you can only pass the right object (where the BlogPostHeader_blogPost fragment has been spread) to useFragment, or you’ll have a type error at build time (in your editor!). As you can see, we take blogPost from props and pass it to useFragment as the second parameter. And if blogPost does not have the right fragment (BlogPostHeader_blogPost) spread on it, we’ll get a type error.

It doesn't matter if another fragment with the exact same data selections has been spread on that object, Relay will make sure it's the exactly right fragment you want to use with useFragment. This is important, because it's another way Relay guarantees you can change your fragment definitions without any other component being affected implicitly.

Relay eliminates another source of potential errors: passing the exact right object containing the right fragment.

You can only use data you’ve explicitly asked for

We define our fragment BlogPostHeader_blogPost on BlogPost. Notice that we explicitly select two fields for this component:

- `title`
- `coverImgUrl`

That’s because we’re using these fields in this specific component. This highlights another important feature of Relay - data masking. Even if BlogPostAuthor_blogPost, the next fragment we’re spreading, also selects title and coverImgUrl (meaning they must be available in the query on that exact place where we'll get them from), we won’t get access to them unless we explicitly ask for them via our own fragment.

This is enforced both at the type level (the generated types won’t contain them) and at runtime - the values simply won’t be there even if you bypass your type system.

This can feel slightly weird at first, but it’s in fact another one of Relay’s safety mechanisms. If you know it’s impossible for other components to implicitly depend on the data you select, you can refactor your components without risking breaking other components in weird, unexpected ways. This is great as your app grows - again, every component and its data requirements become entirely self-contained.

Enforcing that all data a component requires is explicitly defined means you can’t accidentally break your UI by removing a field selection from a query or fragment that some other component was depending on.

      const blogPostData = useFragment<BlogPostHeader_blogPost>(
        graphql`
          fragment BlogPostHeader_blogPost on BlogPost {
            title
            coverImgUrl
            ...BlogPostAuthor_blogPost
            ...BlogPostLikeControls_blogPost
          }
        `,
        blogPost
      );

Here we're using the React hook useFragment to get the data for our fragment. useFragment knows how to take a fragment definition (the one defined inside the graphql tag) and an object where that fragment has been spread (blogPost here, which comes from props), and use that to get the data for this particular fragment.

Just to reiterate that point - no data for this fragment (title/coverImgUrl) will be available on blogPost coming from props - that data will only be available as we call useFragment with the fragment definition and blogPost, the object where the fragment has been spread.

And, just like before, we spread the fragments for the components we want to render - in this case, BlogPostAuthor_blogPost and BlogPostLikeControls_blogPost since we're renderering <BlogPostAuthor /> and <BlogPostLikeControls />.

For the curious: since fragments only describes what fields to select, useFragment won’t make an actual request for data to your GraphQL API. Rather, a fragment must end up in a query (or other GraphQL operation) at some point in order for its data to be fetched. With that said, Relay has some really cool features which will let you refetch a fragment all by itself. This is possible because Relay can generate queries automatically for you to refetch specific GraphQL objects by their id. Anyway, we digress...

Also, if you know Redux, you can liken useFragment to a selector that lets you grab only what you need from the state tree.

      return (
        <div>
          <img src={blogPostData.coverImgUrl} />
          <h1>{blogPostData.title}</h1>
          <BlogPostAuthor blogPost={blogPostData} />
          <BlogPostLikeControls blogPost={blogPostData} />
        </div>
      );

We then render the data we explicitly asked for (coverImgUrl and title), and pass the data for the two children components along so they can render. Notice again that we pass the object to the components where we spread their fragments, which is at the root of the fragment BlogPostHeader_blogPost this component defines and uses.

How Relay ensures you stay performant

When you use fragments, each fragment will subscribe to updates only for the data it’s actually using. This means that our <BlogPostHeader /> component above will only re-render by itself if coverImgUrl or title on the specific blog post it’s rendering is updated. If BlogPostAuthor_blogPost selects other fields and those update, this component still won’t re-render. Changes to data is subscribed to at the fragment level.

This may sound a bit confusing and perhaps not that useful at first, but it’s incredibly important for performance. Let’s take a deeper look at this by contrasting it to how this type of thing is typically done in when dealing with GraphQL data on the client.

With Relay, only the components using the data that was updated will re-render when data updates.

Where does the data come from in your view? Contrasting Relay to other frameworks

All data you use in your views must originate from an actual operation that gets data from the server, like a query. You define a query, have your framework fetch it from the server, and then render whatever components you want in your view, passing down the the data they need. The source of the data for most GraphQL frameworks is the query. Data flows from the query down into components. Here’s an example of how that’s typically done in other GraphQL frameworks (arrows symbolize how data flows):

Note: framework data store is what’s usually referred to as the cache in a lot of frameworks. For this article, assume that "framework data store" === cache.

The flow looks something like:

  1. <Profile /> makes the query ProfileQuery and a request is issued to the GraphQL API
  2. The response is stored in some fashion in a framework-specific data store (read: cache)
  3. The data is delivered to the view for rendering
  4. The view then continues to pass down pieces of the data to whatever descendant components needs it (Avatar, Name, Bio, etc.). Finally, your view is rendered

How Relay does it

Now, Relay does this quite differently. Let’s look at how this illustration looks for Relay:

What’s different?

  • Most of the initial flow is the same - the query is issued to the GraphQL API and the data ends up in the framework data store. But then things start to differ.
  • Notice that all components which use data get it directly from the data store (cache). This is due to Relay’s deep integration with fragments - in your UI, each fragment gets its own data from the framework data store directly, and does not rely on the actual data being passed down to it from the query where its data originated.
  • The arrow is gone from the query component down to the other components. We’re still passing some information from the query to the fragment that it uses to look up the data it needs from the data store. But we’re passing no real data to the fragment, all the real data is retrieved by the fragment itself from the data store.

So, that’s quite in depth into how Relay and other GraphQL frameworks tend to work. Why should you care about this? Well, this setup enables some pretty neat features.

Other frameworks typically use a query as the source of data, and rely on you passing the data down the tree to other components. Relay flips this around, and lets each component take the data it needs from the data store itself.

Performance for free

Think about it: When the query is the source of the data, any update to the data store that affects any data that query has forces a re-render for the component holding the query, so the updated data can flow down to any component that might use it. This means updates to the data store causes re-renders that must cascade through any number of layers of components that don’t really have anything to do with the update, other than taking data from parent components in order to pass on to children components.

Relay's approach of each component getting the data it needs from the store directly, and subscribing to updates only for the exact data it uses, ensures that we stay performant even as our app grows in size and complexity.

This is also important when using subscriptions. Relay makes sure that updated data coming in from the subscription only causes re-renders of the components actually using that updated data.

Using a Query as the source of data means your entire component tree will be forced to re-render when the GraphQL cache is updated.

Modularity and isolation means you can safely refactor

Removing the responsibility from the developer of routing the data from the query down to whichever component actually needs said data also removes another chance for developers to mess things up. There’s simply no way to accidentally (or worse, intentionally) depend on data that should just be passing through down the component tree if you can't access it. Relay again makes sure it does the heavy work for you when it can.

Using Relay and its fragment-first approach means it's really hard to mess up the data flow in a component tree.

It should of course be noted though that most of the cons of the “query as the source of data” approach can be somewhat mitigated by old fashioned manual optimization - React.memo, shouldComponentUpdate and so on. But that’s both potentially a performance problem in itself, and also prone to mistakes (the more fiddly a task, the more likely humans are to eventually mess it up). Relay on the other hand will make sure you stay performant without needing to think about it.

Each component receiving its own data from the cache also enables some really cool advanced features of Relay, like partially rendering views with the data that’s already available in the store while waiting for the full data for the view to come back.

Summarizing fragments

Let’s stop here for a bit and digest what type of work Relay is doing for us:

  • Through the type system, Relay is making sure this component cannot be rendered without the exact right object from GraphQL, containing its data. One less thing we can mess up.
  • Since each component using fragments will only update if the exact data it uses updates, updates to the cache is performant by default in Relay.
  • Through type generation, Relay is ensuring that any interaction with this fragment's data is type safe. Worth highlighting here is that type generation is a core feature of the Relay compiler.

Relay’s architecture and philosophy takes advantage of how much information is available about your components to the computer, from the data dependencies of components, to the data and its types offered by the server. It uses all this and more to do all sorts of work that normally we - the developers who have plenty to do already - are required to deal with.

It's easy to underestimate how quickly views become complex. Complexity and performance is handled by default through the conventions Relay force you to follow.

This brings some real power to you as a developer:

  • You can build composable components that are almost completely isolated.
  • Refactoring your components will be fully safe, and Relay will ensure you’re not missing anything or messing this up.

The importance of this once you start building a number of reusable components cannot be overstated. It’s crucial for developer velocity to have refactoring components used in large parts of the code base be safe.

As your app grows, the ease and safety of refactoring becomes crucial to continue moving fast.

Wrapping up our introduction to Relay

We’ve covered a lot of ground in this article. If you take anything with you, let it be that Relay forces you to build scalable, performant, type safe applications that will be easy and safe to maintain and refactor.

Relay really does do your dirty work for you, and while a lot of what we’ve shown will be possible to achieve through heroic effort with other frameworks, we hope we’ve shown the powerful benefits that enforcing these patterns can bring. Their importance cannot be overstated.

A remarkable piece of software

Relay is really a remarkable piece of software, built from the blood, sweat, tears, and most importantly - experience and deep insight - of shipping and maintaining products using GraphQL for a long time.

Even though this article is pretty long and fairly dense, we've barely scratched the surface of what Relay can do. Let's end this article with a list detailing some of what more Relay can do that we haven't covered in this article:

  • Mutations with optimistic and complex cache updates
  • Subscriptions
  • Fully integrated with (and heavily leveraging) Suspense and Concurrent Mode - ready for the next generation of React
  • Use Relay to manage your local state through Relay, enjoying the general benefits of using Relay also for local state management (like integration with Suspense and Concurrent Mode!)
  • Streaming list results via @stream
  • Deferring parts of the server response that might take a long time to load via @defer, so the rest of the UI can render faster
  • Automatic generation of queries for refetching fragments and pagination
  • Complex cache management; control how large the cache is allowed to get, and if data for your view should be resolved from the cache or the network (or both, or first the cache and then the network)
  • A stable, mature and flexible cache that Just Works (tm)
  • Preload queries for new views as soon as the user indicates navigation is about to happen _ Partially render views with any data already available in the store, while waiting for the query data to arrive
  • Define arguments for fragments (think like props for a component), taking composability of your components to the next level
  • Teach Relay more about how the data in your graph is connected than what can be derived from your schema, so it can resolve more data from the cache (think "these top-level fields with these variables resolve the same User")

This article ends here, but we really encourage you to go on and read the article on pagination in Relay. Pagination in Relay bring together the powerful features of Relay in a beautiful way, showcasing just how much automation and what incredible DX is possible when you let a framework do all the heavy lifting. Read it here

Here’s a few other articles you can continue with too:

Thank you for reading!

Special thanks

Many thanks to Xavier Cazalot, Arnar Þór Sveinsson, Jaap Frolich, Joe Previte, Stepan Parunashvili, and Ben Sangster for thorough feedback on the drafts of this article!

Discussion (11)

pic
Editor guide
Collapse
hegelstad profile image
Nikolai Hegelstad

Fantastic article, thank you Gabriel. I think we are blessed to have Facebook's front-end tech stack readily available to us. When Concurrent Mode and Suspense hits production, it will be a game-changer for sure.

Collapse
zth profile image
Gabriel Nordeborn Author

Thank you Nikolai! Yes for sure, that will change how we build UI for the better. I've experimented quite a lot with the new APIs, and it's pretty amazing the control it gives you over your UI. Looking forward to the continuation of this!

Collapse
daniel15 profile image
Daniel Lo Nigro

This is such a great writeup! I've seen other articles where people complain about Relay being too opinionated, but you really understand why it's built the way it is. Fantastic work.

In my team at Facebook, we just started using Relay a year or two ago, and I don't think we could ever go back to the "old way" we used to do things. Relay is so good.

Collapse
rosavage profile image
Ro Savage

Great article, thanks Gabriel. Love these in-depth posts!

One thing I've never understood about relay is how to have a fragment request data that the fragment above it doesn't know about.

In your example, imagine the fragment that contains the tags. It would look something like

      const blogPostTags = useFragment<BlogPostHeader_blogTags>(
        graphql`
          fragment BlogPostTags_blogPost on BlogPost {
            tags  {
                 slug
                 shortName
             }
          }
        `,
        blogPost
      );

Now, imagine from this component we also wanted to be able to change add a new tag. So now we need not just the tags that the blog has, but all available tags.

If weren't using Fragments we could write a top level query that might look like

blog {
   tags { 
     slug
     shortName
   }
   blogPostById(id: $blogPostId) {
     tags {
       slug
       shortName
     }
   }
}

However, because we are using relay fragments the only way would be to pass down account from our top level query. But the BlogPost and BlogHeader don't require account. So we end up just passing account through two components that have no idea why they require account.

Now you can take this futher, maybe you have a <TagPicker /> component that can be re-used everywhere. It only requests tags { slug, shortName } but can be dozens of components down from the top level query where Blog {} is originally asked and where the fragment needs to be destructured.

Is there a nice way Relay can solve this?

Three solutions I've used is
1) Have the <TagPicker /> have query the graphql endpoint directly. But then I have unnecessary extra requests and repeated requests for the exact same data.
2) Have the <TagPicker /> use context to have the relay $key passed down without prop-drilling. However this causes race conditions when relay receives new data, and expects that the component will have it but because context hasn't re-rendered yet it's actually empty.
3) Prop-drilling down a dozen components, which makes refactoring very hard.

Collapse
julioxavierr profile image
Júlio Xavier

Great job guys! Really well written!

Collapse
zth profile image
Gabriel Nordeborn Author

Thank you very much Júlio!

Collapse
beingbook profile image
Han BaHwan

the link dev.to/zth/connection-based-pagina... looks broken. is there any alternative post you recommend?

Collapse
zth profile image
Gabriel Nordeborn Author

Hi! Sorry about the links, they should point to the right place now. Here's the real one: dev.to/zth/connection-based-pagina...

Thanks for reading!

Collapse
beingbook profile image
Han BaHwan

official document itself might be good resource perhaps?

Collapse
zth profile image
Gabriel Nordeborn Author

Oh yes, for sure, the official docs are a great resource!

Collapse
stevndegwa profile image
Stephen N.

Thank you so much, Gabriel. This article gave me a great understanding of relay. Now it'll be easier to understand the docs.