Using Fragments
Recommended background reading
Videos from the Relay video series covering fragments:
Using Fragments
One of the main things that make Relay so powerful is using GraphQL fragments to co-locate the data-demands of your components with your actual component code. This leads to well-isolated components that are very portable and easy to maintain.
A high-level overview of fragments
Fragments are GraphQL snippets that can be reused throughout your GraphQL operations. At it's core, a fragment is a selection of fields on a specific GraphQL type.
Let's examplify through a basic query, first without fragments:
query {
  me {
    firstName
    lastName
    friendCount
    avatarUrl
  }
}
Here's a very basic query that selects a bunch of fields on the logged in User. Let's look at the same query, but using fragments representing the various components that would display the data:
fragment UserNameDisplayer on User {
  firstName
  lastName
}
fragment Avatar on User {
  firstName # needed for a nice alt-tag
  lastName
  avatarUrl
}
fragment FriendCountDisplayer on User {
  friendCount
}
query {
  me {
    ...UserNameDisplayer
    ...Avatar
    ...FriendCountDisplayer
  }
}
See the difference? We've split our data demands on me into fragments responsible for displaying a certain part of the User, much like we'd do with React components, delegating displaying different parts of the UI to different, specialized components.
If you want to dive deeper into GraphQL fragments you're encouraged to read through the official documentation on fragments in GraphQL.
Fragments in RescriptRelay
Fragments are defined in RescriptRelay by using the %relay() extension node. Here's an example of a fragment and a component that renders the fragment data:
/* UserProfileHeader.res */
module UserFragment = %relay(
  `
  fragment UserProfileHeader_user on User {
    firstName
    lastName
  }
`
)
@react.component
let make = (~user) => {
  let user = UserFragment.use(user)
  <div> {React.string(user.firstName ++ (" " ++ user.lastName))} </div>
}
A note on naming: Due to the rules of Relay, a fragment must be named
<ModuleName><optionally_anything_here>_<identifier>, where module name here means file name, not ReScript module name. So for a fileUserProfile.res, all fragments in that file must start withUserProfileregardless of whether they're defined in nested modules or not. Identifier can be anything, but it's common to name that after the GraphQL type the fragment targets, lowercased. So a fragment inUserProfile.resthat targets theUser, would commonly be calledUserProfile_user.
Using VSCode? Our dedicated VSCode extension lets you codegen new fragments (and optionally boilerplate for a component) via the command
> Add fragment.
Let's break down what's happening here:
- We define a fragment on the GraphQL type Userthrough%relay().
- We define a React component that takes a userprop.
- %relay()with a fragment defined in it will autogenerate a React hook called- use, which takes any object containing a fragment reference for that particular fragment, and returns the data.
- Just as with queries, usefor fragments is integrated with React suspense, meaning thatusewill suspend if the data's not already there.
Fragment references and how Relay transports fragment data
A fragment always has to end up in a query at some point for it to be able to render. This is quite simply because a fragment is a specification of what data is needed, and somebody (I'm looking at you query) needs to take this specification and get the actual data from the server. Let's tie together the sample fragment code from above with the sample code from making queries in order to demonstrate how components with fragments are used with other components, and how the fragment ends up in a query:
/* UserProfile.res */
module Query = %relay(
  `
  query UserProfileQuery($userId: ID!) {
    userById(id: $userId) {
      ...UserProfileHeader_user
    }
  }
`
)
@react.component
let make = (~userId) => {
  let queryData = Query.use(
    ~variables={
      userId: userId,
    }
  )
  switch queryData.userById {
  | Some(user) => <UserProfileHeader user=user.fragmentRefs />
  | None => React.null
  }
}
Let's break down what has changed:
- In order for us to be able to render <UserProfileHeader />in<UserProfile />, we need to provide it with the data it needs from theUsertype. It defines what data it needs fromUservia the fragmentUserProfileHeader_user, so we spread that fragment on aUserobject in our query. This will ensure that the data demands onUserfor<UserProfileHeader />is fetched in the query.
- When we get the data from Query.use, the object where we spreadUserProfileHeader_userwill include a fragment reference for that fragment. Fragment references are how Relay carries the data for fragments, and each fragmentusehook knows how to take afragmentRefsprop containing its own fragment reference and use it to get its own data.
Any object (it's actually a ReScript record, but I'll call it object here) where one or more fragments have been spread will have a prop called fragmentRefs. That prop will contain all fragment references for all fragments spread. Incidentally, this is exactly what the respective fragment's use hook wants!
- We make sure we actually got a user, and then we take the userByIdobject (where we spreadUserProfileHeader_user), and pass the fragment references to<UserProfileHeader />viauserById.fragmentReferences. That component then passes that to the fragmentUserProfileHeader_userusehook, which then exchanges it for the actual fragment data.
Phew! That's a lot to break down. It's really not that complicated to use though.
Conditional fragments and more with the @alias directive
Recommended background reading: https://relay.dev/docs/guides/alias-directive/
Notice that by default all fragment references on the same object are nested within the same fragmentRefs property. This is convenient because you can pass that fragmentRefs to any component that expects fragment data on that object, without having to care about pulling out that specific fragment's data.
However, there are a few scenarios where this causes problems:
- Conditional fragments ...SomeComponent_someFragment @include(if: $someVariable). There's no way to figure out if this fragment was actually included or not without also knowing the value of$someVariable.
- Abstract types and fragment spreads. Read more in the Relay docs.
@alias solves these problems by putting each fragment with @alias on its own property. So, for the SomeComponent_someFragment example above, instead of referencing fragmentRefs to access the fragment reference for SomeComponent_someFragment, you'd find it under a property called someComponent_someFragment. And that property will be optional and not present if the fragment is not included.
This solves a major safety issue in Relay, as well as enable things like easy conditional fragments.
A full example of what it looks like in RescriptRelay:
/* UserProfileHeader.res */
module UserFragment = %relay(
  `
  fragment UserProfileHeader_user on User {
    firstName
    lastName
    ...Avatar_user @alias
  }
`
)
@react.component
let make = (~user) => {
  let user = UserFragment.use(user)
  <div>
    <Avatar user=user.avatar_user /> {React.string(user.firstName ++ " " ++ user.lastName)}
  </div>
}
There's more fancy stuff you can do with @alias, including controlling what the property name for the fragment ends up as in the types. You're encouraged to read the official Relay docs to learn more.
Also, please note that if you're under version 3.1.0 of RescriptRelay you'll need to enable @alias in your Relay config:
// relay.config.js
module.exports = {
  schema: "./schema.graphql",
  artifactDirectory: "./src/__generated__",
  src: "./src",
  featureFlags: {
    enable_relay_resolver_transform: true,
    enable_fragment_aliases: {
      kind: "enabled",
    },
  },
};
Fragments in fragments
Yup, you read that right, you can spread fragments on other fragments. Remember, a fragment at some point must end up in a query to be usable, but it doesn't mean that each fragment must be spread on a query.
Let's expand our example fragment component to use another component <Avatar /> that is responsible for showing a an avatar for a user:
/* UserProfileHeader.res */
module UserFragment = %relay(
  `
  fragment UserProfileHeader_user on User {
    firstName
    lastName
    ...Avatar_user
  }
`
)
@react.component
let make = (~user) => {
  let user = UserFragment.use(user)
  <div>
    <Avatar user=user.fragmentRefs /> {React.string(user.firstName ++ " " ++ user.lastName)}
  </div>
}
See the difference? Let's break it down:
- We want to render <Avatar />, and it needs data fromUser. So, we spread its data demands on the user type that we're already getting data for. That will create a fragment reference forAvatar_useron theuserrecord we get back fromUserFragment.use.
- We then pass userData.fragmentRefsto<Avatar />.<Avatar />then uses that to get the data it needs fromUser.
We don't have to change anything anywhere else. <UserProfile />, who defines the query and fetches the data, does not need to know anything about this change. It just knows that it needs to get the data for UserProfileHeader_user - it's not concerned with how that data looks or if it includes more fragments. It just gets the data for UserProfileHeader_user, passes it along and minds its own business.
This is a core strength of Relay called data masking - no data is available to anyone unless they explicitly ask for it. You can read more about data masking in Relay here.
Arguments in fragments
Fragments can define arguments. The Relay docs on arguments in fragments explain the concept well enough, so we'll just focus on the differences between RescriptRelay and regular Relay.
Provided variables in RescriptRelay
Provided variables is a Relay concept that lets you provide constant values (where the value itself is resolved from ReScript) as variables to a fragment. This removes the need to orchestrate adding your constants as regular query variables in every single query where you'd like to use them. It also encapsulates the variable so that the fragment is the only thing that needs to care about it.
Here's an example of how using provided variables in RescriptRelay looks:
// SomeModule.res
module Fragment = %relay(`
  fragment SomeModule_user @argumentDefinitions(
    pixelRatio: {type: "Float!", provider: "MyProvidedVariables.PixelRatio"}
  ) {
    name
    avatar(pixelRatio: $pixelRatio)
  }
`)
// MyProvidedVariables.res
module PixelRatio = {
  let get = () => getDevicePixelRatio()
}
Let's distill it:
- We're defining a pixelRatioargument for our fragment, and give it aprovider.
- The providerargument points to a ReScript module. This module should define agetfunction that takesunitand returns the same type as is defined insidetypeof the argument. So for the example above, that islet get: unit => float. The compiler will enforce that you get the types right.
- Relay will then automatically wire together MyProvidedVariables.PixelRatiowith the fragment.
Using fragments outside of React's render phase
You can also use fragments outside of React's render phase (read: without using hooks). In addition to Fragment.use, each fragment will autogenerate a function called Fragment.readInline if your fragment is annotated with @inline.@inline tells Relay you'll want to read this fragment outside of React's render phase.
This works the same way as Fragment.use as in you feed it an object with a fragment reference for that particular fragment. But, when you run the function, you'll get a one-shot snapshot of that fragment data from the store as it is right now.
Great for logging and similar activities. Example:
/* SomeCoolLogger.res */
module UserFragment = %relay(
  `
  fragment SomeCoolLogger_user on User @inline {
    customerId
    someOtherMetaDataProp
  }
`
)
let logPurchase = user => {
  /* We read the fragment data from the store here, without needing to use a hook */
  let userData = UserFragment.readInline(user)
  SomeLoggingService.log(
    ~customerId=userId.customerId,
    ~someOtherMetaDataProp=userId.someOtherMetaDataProp,
  )
}
/* BuyButton.res */
@react.component
let make = (~user) =>
  <button
    onClick={_ =>
      /* user here contains the fragment reference for SomeCoolLogger_user defined above */
      SomeCoolLogger.logPurchase(user)}>
    {React.string("Buy stuff!")}
  </button>
On to the next thing
That's a basic introduction to fragments. There are a few more concepts around fragments that are worth spending some time to grok. However, none of them are specific to using Relay with ReScript, so you can read more about them in the Relay documentation below.
Before we move on to the next thing, there's a few things that's worth keeping in mind about fragments:
- Use fragments as much as you can. They are optimized for performance and help promote well contained and isolated components
- A component can use any number of fragments, not just one
- A fragment can use other fragments
- Any object where a fragment has been spread will have a prop called fragmentRefs. This contains references for all fragments that have been spread on that object. You pass thatfragmentReferencesprop to the respective fragment'susehooks.
- @aliaslet's you know for sure whether a fragment has been included (and matches) or not.
With that in mind, Let's jump in to mutations.
API Reference
%relay() is expanded to a module containing the following functions:
use
SomeFragment.use is a React hook that takes an object containing a fragment reference for that particular fragment, and returns the fragment data.
useuses Relay'suseFragmentunder the hood, which you can read more about here.
readInline
Your fragment needs to be annotated with
@inlinefor this function to appear.
SomeFragment.readInline is a function that takes an object containing a fragment reference for that particular fragment, and returns the fragment data. Can be used outside of React's render phase.
readInlineuses Relay'sreadInlineDataunder the hood.