Menu
Defer is reaching the end of its service life on May 1, 2024. Please reach out to support@defer.run for further information.
Engineering
March 4th, 2023

Relay GraphQL:
the choice of a stable front-end

Charly PolyCharly PolyCEO

While GraphQL's unique developer experience is being challenged by new technologies such as tRPC or Remix, it remains the best choice for building a rich front-end, especially when the API lives in a separate repository.

Initially brought by Apollo GraphQL, its end-to-end type safety, powerful cache features, and data hooks patterns totally removed the need for a complex reducer or custom caching implementations.


As an early GraphQL adopter and advocate, I started building the Defer Dashboard with GraphQL on top of our back-end implemented in Go.


However, having used GraphQL for years, I wasn't sure that doing so with Apollo GraphQL or URQL remained the go-to solution.



Apollo Client and URQL drawbacks

During the last 6 years I have been building SPAs with Apollo Client, a pattern has progressively emerged, revealing the same drawbacks over again.


The great useQuery() hook allowed us to learn and build GraphQL front-end quickly; however, after months, most applications end up with the same issues:

  • Re-rendering performance issues
  • Struggles to narrow down the initiator of unwanted GraphQL requests
  • Hard to reproduce and fix local cache optimization updates

With time, I concluded that those drawbacks were almost inevitable symptoms of fast-growing GraphQL applications.

However, during my time at The Guild, Laurin introduced me to a different approach to GraphQL front-end architecture: the original vision of Facebook.


Although being a great pattern, the useQuery() hook hides a design flaw: GraphQL requests are triggered based on the React rendering lifecycle.

The recent discussions around the useEffect() hook have highlighted how complex understanding the React rendering lifecycle is.


By tying the GraphQL requests to the React rendering lifecycle, narrowing down the origin of a GraphQL request is like searching for a needle in a haystack.

Even worse, the completion of a GraphQL request triggers an update on the component, playing a complex game of rendering feedback loop:

Facebook's (Relay team) principles for GraphQL front-end architecture are the following:

  • GraphQL requests should be initiated from the application router, not from the components.
  • Each component should define a GraphQL Fragment, which should be included in the page's main Query.

Those principles allow for scalability at the performance and maintainability level of a GraphQL front-end, preventing GraphQL requests from being triggered in an unpredictable way and leveraging fragments to define a component's data requirements.


As I started working on a new front end, I applied these principles and used Relay as the GraphQL client for the Defer Dashboard. Here is how it turned out.



Relay and the Defer Dashboard

Coming to Relay from Apollo is like learning German as a French person.

They look close, but there are many new fundamentals to learn before being fluent in Relay.


Let's cover the main differences with Apollo/URQL:

Embrace the declarative APIs

Relay's primary goal is to help build scalable GraphQL front-ends, making the developer experience a secondary - or 42nd - concern.


For this reason, Relay provides a very declarative API using many “Java-esque” names: Environment, PreloadedQuery, QueryLoader, or useRefetchableFragment().

While these can be repulsive at first, they quickly help you understand how Queries are used in a declarative way that prevents unwanted side effects.

Trigger GraphQL requests at the routing level

The key principle of Relay resides in triggering GraphQL requests from the application's router when a user is navigating to another route.


No React routing library has been providing such capabilities until recently, when React Router v6 exposed a new loader API. Here is an example:

const router = createBrowserRouter([  {    loader: async () => {      return loadQuery(environment, getCurrentUser, {})    },    id: "root",    element: <Outlet />,    errorElement: <ErrorPage />,    children: [      // ...    ],  },])

The Router triggers GraphQL requests in a Relay application and forwards GraphQL Query references through its React Context (useLoaderData()).


React components then retrieve the GraphQL Query's fetched data using the provided GraphQL Query reference.

export const ApplicationBuildListRenderer = () => {  const preloadedQuery = useLoaderData()
  const data = usePreloadedQuery(    getApplicationBuildListQuery,    preloadedQuery  )
  return (    <Suspense>      <ApplicationBuildListView application={data.application} />    </Suspense>  )}

Triggering GraphQL Requests at the router level brings two main benefits:

  • Knowing when and why a GraphQL Request is triggered is predictable, thanks to a declarative approach
  • By the time your components renders, their associated GraphQL Query might already be resolved, preventing re-renderings


Finally, a real use case for React Suspense

Relay and React Suspense are meant to be together.

A React Component cannot “consume” a GraphQL Query reference without being wrapped in a <Suspense> container.


Covering React Suspense would deserve its own article, but let's just say that Suspense is a remake of the Container/View pattern with a nice declarative way to handle loading states.

However, it leads to a lot of boilerplate code, and complex components loading a lot of data (ex: organization + repository selector component).

Thinking in Fragments

As mentioned earlier, GraphQL requests get triggered at the routing level.

So, how do you implement a pagination-based component or a component that needs to refresh every 30s?

Before answering this question, let's take a closer look at GraphQL Fragments.


Relay and Apollo treat GraphQL Fragments differently.


With Apollo, you learn that GraphQL Fragments are a productivity feature, meant to avoid duplicates across GraphQL Queries.

In Relay, GraphQL Fragments serve as a core part of the architecture: they declare the data dependencies of components:

export const BuildBanner: FC<BuildBannerProps> = ({  applicationKey,  buildKey,}) => {  const params = useParams()  const build = useFragment(    graphql`      fragment BuildBanner_LastBuild on Build {        id        createdAt        state        gitCommitSHA1        gitCommitAuthorAvatar        gitCommitAuthorName        gitCommitAuthorUsername      }    `,    buildKey  )
  // ...}

Relay leverages GraphQL directives to declare Fragments as subqueries you can perform later, with Refetchable Fragment and Pagination Fragment.


Here's how we have implemented a component that lists and refresh GitHub organizations inside the application creation form:

// ...let [data, refetch] = useRefetchableFragment(  graphql`    fragment OrgAndRepos_Fragment on Account    @refetchable(queryName: "ReposSelector_Orgs") {      organizations {        edges {          node {            id            name          }        }      }    }  `,  viewer)// ...


Relay: an honest feedback

Let's be honest; coming from Apollo Client, the first days of using Relay were a struggle.

I wanted to lazy load queries everywhere and move faster by using shortcuts,

but Relay was not allowing it.


Once the base architecture and good habits are in place, adding new features gets pretty quick, and performance in production is satisfying.



The deceptive side

While the Relay documentation does a good job at covering high-level concepts, it gets blurry on advanced ones, such as the API references or examples.

Most examples are inaccurate or incomplete, leaving you alone with your challenges.

For example, we faced issues while trying to implement a simple page-per-page pagination view.


Relay only provides examples for infinite scroll pagination, so we had to build our own pagination, leveraging the useRefetchableFragment() and pagination variables.

(We plan to give back to the Relay community by submitting documentation improvements)



Relay is the purest GraphQL client and it will do everything to slow you down from doing things the non-GraphQL way. For example, Relay makes implementing live paginated queries without Subscriptions hard.

Along the same line, GraphQL Relay enforces strong GraphQL naming conventions. Each GraphQL Query, Mutation, and Fragment needs to be prefixed by the filename and be suffixed by “Query” or “Mutation”.


Not following those conventions will result in errors raised by the Relay compiler (and associated TypeScript types).



The bright side

Relay's strong opinions come with some advantages.

It ensures that your front end will stay performant and properly organized.


In the same way that ESLint provides some “React hooks rules” rules, you can see Relay as your best ally in scaling - on the human and technical side - your front-end.

Relay patterns such as useRefetchableFragment() or usePaginationFragment() are underrated features; they actually remove most of the Apollo Client/URQL boilerplate required to achieve similar behavior.

As mentioned multiple times, Relay's API makes any data actions explicit, by removing any implicit side effects, and giving access to a spectrum of advanced options (ex: the Relay cache API is very powerful - and poorly documented).


This enables us to collaborate with standard practices and clear performance expectations confidently.



Conclusion

Many will argue that Relay only belongs to Facebook's website codebase, but at the same time, too many early-stage startups are struggling with Apollo Client/URQL-based front-end SPAs.

Most companies end up putting in place conventions and architecture rules similar to those Relay advocates for.


Relay will initially slow you down but will make you win in the long term.


Is Relay the perfect GraphQL client? No.

The documentation has improved a lot but is still way too thin, and many questions on building a SPA with GraphQL remain unanswered.

We expect the Relay team and community to document better some advanced use cases and APIs (ex: Entrypoints), and we look forward to helping achieve this.



Further reading

Covering Relay's cool aspects in a single article were not realistic.


Here are some of the coolest Relay exclusive features, along with a video that sums up the principles of Relay architecture:


Explain Relay's architecture principle clearly with Robert Balicki's talk: Re-introducing Relay.

Join the community and learn how to get started, and provide feedback.
Stay tuned about our latest product and company updates.
Start creating background jobs in minutes.
Copyright ©2024 Defer Inc.