Scaling frontend app teams using Relay

The UI is decomposed into multiple React components.

The basic idea behind scaling the frontend, much like scaling any other part of the stack, is factoring it into multiple components:

  • Can be owned by multiple independent specialised teams
  • Reduce coupling between components
  • Have clearly specified interfaces between components

Let's look at how this strategy plays out, and how to use modern technologies and ideas like Relay and backend-for-frontend (BFF) to scale out frontend applications and teams.

Independent isolated teams own different components

This is a useful baseline from which to iterate. It represents the naive scenario in which teams independently develop different components in a larger app but in isolation from each other.

Independently developed components with independent data fetching. The circles are different teams.

Problem: Independent data fetching

  • Poor performance and UX jank
  • A component may make multiple network requests
  • Component data request may depend on ancestor data, leading to waterfall style requests
  • Components may fetch redundant data

Problem: Independent state management

  • Data and UI inconsistency

Batching of network requests

Teams can manually coordinate data fetching:

  • Batch all data requests at the root level
  • Distribute data through the component tree via props
Teams manually batch data requests through a shared root level query.

Problem: Coupling at root query

  • Adding a query: can duplicate other similar queries, and fetch redundant data
  • Removing or editing a query:
  • Can break another component that (implicitly) depends on the same data
  • Not removing unused data leads to over-fetch and cruft

Problem: Poor developer ergonomics

  • The data requirements for a component are no longer colocated with the component itself, which breaks encapsulation

TRPC / React Query is not a complete solution

  • Batches parallel network queries into single network requests
  • Cannot batch all queries needed for a page
  • Cannot solve waterfall requests
  • Queries are batched over the network layer, but still execute against the data layer as independent queries i.e. batching cannot leverage the internal structure or relations of queries

Coordinate data access through a centralized cache store

Teams manually coordinate shared state through a central store.
  • Introduces coupling between teams, with similar issues as with batching queries
  • Lots of boilerplate to normalize data, update stores, and plumb data to components

Backend for frontend

BFF is useful for making lighter and more performant client applications by moving compute and data transformations to the server.

Backend for frontend: Moves compute and data transformations to the server. Collectively owned by all frontend teams.
  • Owned by the client app team
  • Doesn't solve any of the coupling problems, just moves it around

GraphQL

Batched queries organized around client pages instead of server functionality couple the frontend and the backend teams.

Backend developers build APIs organized around Frontend pages/routes.

Instead, a GraphQL API exposes all features of the backend at a single endpoint.

  • The client app can craft queries that fetch exactly the data needed in one shot
Backend GraphQL API organized around server capabilities allows for flexible frontend queries.

Even better, we begin to see that the query structure beings to mirror the component tree!

  • This means we can refactor a root level query into fragments
Queries decompose into fragments.

Putting it all together with Relay

In Relay, every component defines its own data requirements

Data dependencies colocated with components. Fragment structure mirrors the component tree.
  • Significantly, a component can only access the data it has explicitly requested. This is "data masking" and is enforced through the useFragment hook.
  • This means that teams can modify individual components with confidence knowing that nothing will break as there are no implicit data dependencies
Independently declared data dependencies are compiled into a single root level query by the Relay compiler.

At build time, the Relay compiler builds an optimized set of top level queries from all the fragments

  • The compiler can check for common errors, and run optimizations such as deduplication across the whole codebase
  • You get the developer ergonomics of independently developed components, but with the efficiency of globally optimized and batched data fetching
Relay can leverage the rich information present in the GraphQL schema. Incrementally adopt the global node id and connection spec.

Relay uses the rich type information in the GraphQL schema, and automatically builds a local cache of data from all queries.

Further, by incrementally adopting features such as the node global id spec and the connection spec, you get advanced features such as:

  • Reloading only a portion of a query via fragments
  • Cursor based pagination

Conclusion

The JavaScript ecosystem has produced a variety of solutions for frontend development, data fetching, and state management; but solutions often fall short for ambitious projects.

GraphQL, Relay, and React were built to work together and have the huge benefit of being driven by Meta's (Facebook) experience building and maintaining extremely large and and complex applications developed by many teams.

It seems that GraphQL and React has taken over the world, but people are often put off by the new conventions espoused by the Relay library.

The good news is that Relay can be incrementally adopted.

  • The client library will work with any existing GraphQL API
  • Adopting bits of the Relay spec unlocks additional features
  • Relay can be adopted selectively by some components and not others, for an easy migration path

Even better, it seems that the people who've implemented Relay, have been happy with it for quite a while.

Use Hasura to easily generate a Relay compatible API with no code, across multiple data stores, with cross data store joins, filtering, and aggregations, and declaratively defined permissions.

Sign up now for Hasura Cloud to get started!

Blog
31 Aug, 2023
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.