GraphQL
React Suspense
Defer Dashboard

Relay GraphQL:
the choice of a stable front-end

Learnings from a journey from Apollo to Relay

March 4th, 2023
Posted by
Charly Poly - CEO
Charly Poly - CEO
Spread the word

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.


Being an early GraphQL adopter and advocate, I decided to start 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 came to the conclusion 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, and the latter should be included in the main Query of the page.

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 to work on a new front end, I applied these principles and chose to use 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 in reality, 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 main 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 from 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<srcGetCurrentUserQuery>(environment, getCurrentUser, {});
},
id: "root",
element: <Outlet />,
errorElement: <ErrorPage />,
children: [
// ...
]
}
])

In a Relay application, the Router triggers GraphQL requests 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 ApplicationBuildListQueryRenderer = () => {
const preloadedQuery =
useLoaderData() as PreloadedQuery<ApplicationBuildListQuery>;
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 in order 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 OrgAndRepositorySelectors_OrganizationsFragment on Account
@refetchable(queryName: "RepositorySelector_Organizations") {
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 it pretty hard to implement live paginated queries without using Subscriptions.

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 work as a team with confidence, common practices and clear performance expectations.



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 slow you down at first 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 of the Relay team and community to better document some advanced use cases and APIs (ex: Entrypoints), and look forward to helping achieve this.



Further reading

Covering all the cool aspects of Relay in a single article was not realistic.


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


Get a clear explanation of Relay's architecture principle with Robert Balicki's talk: Re-introducing Relay.

Spread the word
Copyright ©2023 Defer Inc.