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.
if you’re not composing GraphQL fragments from multiple components into one query (as Relay does), i think you’re missing 80% of the point of GraphQL.
which is ok but isn’t talked about enough https://t.co/ooohCDV6ZO
— danabramov.bsky.social (@dan_abramov) March 12, 2023
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.
How Coinbase is scaling their app with Relay
"Relay is unique among GraphQL client libraries in how it allows an application to scale to more contributors while remaining malleable and performant."
https://t.co/D45yEid8l0— Relay (@RelayFramework) May 6, 2022
Join us for a live Q&A session with Tanmai Gopal on the @GraphQL Discord. 🎙️ Discover how to scale UI development with Relay #GraphQL.
⏰ July 12, 11AM PT
Join GraphQl Discord server ➡️ https://t.co/MM3HN7DpbW pic.twitter.com/hhKYYcnbKc— Hasura (@HasuraHQ) July 7, 2023
It still surprises me how Twitter embraces GraphQL / Relay (for RWeb) but keeping their Timeline model instead of adapting the Relay connection spec https://t.co/skOcPvrmMv
— Jane Manchun Wong (@wongmjane) November 19, 2022
💡 #RelayJs feature I appreciate - Relay is optimized for performance by default. It “forces” you to break down the data requirements of your UI in small, reusable parts, like React does with components. It'll then subscribe to changes of only the data each component asks for 1/x
— Gabriel Nordeborn (@___zth___) April 3, 2022
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.
Top comments (0)