Sorry for taking so long to respond. It’s been a busy week and I also have enough material to give a whole talk on this lol.
Could you explain that a bit more for me?
So, the problem with useFragment
is that it fails unrecoverably if the data it needs isn’t in the cache, so you need to make sure that wherever it is used there has previously been a query fetching that fragment. There is, to my knowledge, no automated way to check this, so it’s a manual check by each developer when using a component. This by itself would be somewhat tedious but it gets especially dangerous because checking isn’t at all straightforward for a few reasons:
- Component trees can be quite complex, especially with things like feature flags potentially branching things at multiple points. Dozens or even hundreds of components might be involved, and it becomes very hard to know that you haven’t missed one.
- You can’t just run the code and check if it fails, because data for the fragment in question might be in the cache even if the fragment was never fetched, if its requirements are fulfilled by other fragments. This can result in very subtle bugs. For example, it might be that a screen that uses a given fragment works perfectly fine in the usual route to get to that screen because a previous screen coincidentally fetches the required data but in a rare edge case where users get to the screen from a different route, the cache is not populated and
useFragment
fails. This is not a hypothetical, it’s an entire class of real bugs with Redux “request and store” implementations that I’ve experienced and which are one of the main reasons in my mind for choosing a system like Apollo that guarantees a way to resolve component data requirements instead of relying on other components to have done data fetching before.
One of the biggest advantages is the reduction in network requests. One of the main selling points of GraphQL is the “load only the data you need” aspect of it. If you have multiple queries that load overlapping data, you’re not only sending multiple network requests, but you’re transferring network bytes for data that you’ve already loaded somewhere else.
It’s funny you say this because in my experience fragment composition increases network requests and data transferred over the network. Imagine the following:
Screen A needs Fragments A and B
Screen B needs Fragments A, B, and C
Screen C needs Fragments A, B, and D
If a user is navigating A => B => C then because there’s never perfect overlap between requirements then fragment composition will make three requests:
- AB
- ABC
- ABD
This means requesting A and B three entire times, despite the fact that that data is in cache after the first request. Meanwhile, if each fragment was fetched in a different query, the fragments we fetched would be:
- AB (two queries, but they’re easy to batch into one connection either with a Link or just HTTP2)
- C
- D
This is a lot less data fetched, especially because this pattern has a tendency to repeat across an entire application: it’s very common for features to all require some core fragments (user data, basic account data, etc.) but then differ in what small amount of additional data they need for their specific function.
From a UX perspective, its also a much nicer user experience if you only see a single spinner rather than several spinners on the page because it reduces the “popcorn” effect you get due to varying network latencies between queries (Suspense certainly helps here too though). Far too often I feel like we hurt the UX because “my components can load their own data so I can keep them isolated from each other”. It certainly is great to code that way, but often this comes at the cost of good UX.
I mean, yes, but that’s why the React team invented Suspense, no? There’s also other ways around this, you can mount a non-reactive version of all the queries at the top level of the screen and use them to control unified loading. It’s an easy problem to notice in testing if it starts being disruptive to UX and an easy problem to solve if so. Since it’s not always applicable (often you want things to load staggered, if they’re under the fold) and easy to solve when it is I wouldn’t really want to structure my whole app to work around it.
I’ve seen some usages of Apollo Client that result in 1000+ useQuery
hooks mounted on a single page at the same time. While this isn’t 1:1 with network requests, that page still executed about 20-30 network requests. This absolutely has an effect on UX and perceived performance of the page. We think this goes against some of the core principles that make GraphQL great to work with.
Are 20-30 network requests worse than one big network request fetching 20-30 fragments? Especially with HTTP2 multiplexing meaning that requests can be parallelized very cheaply, there’s distinct advantages to making more smaller queries rather than fewer large ones, like easier error handling and smoother load distribution on the backend.
I have a very different approach that I have successfully implemented in multiple large production applications: don’t have queries defined per component, but rather a single shared library of non-overlapping queries defined per domain entity. This is because:
- Fetching more fields from a single entity is, within reason, really cheap, so it’s often a good tradeoff to fetch a few more fields than necessary upfront so they’re cached and you don’t have to reload the whole entity for just one or two extra fields in the future.
- We tend to think about things in entity terms, anyway, since fields are too granular to fully keep track off. e.g. “this component needs the user’s Account and Preferences”
- Having fewer queries that are shared maximizes the usefulness of both the cache and query deduplication. The objective is that any given piece of data only needs to be requested once per user session. When a user is spending more than a few minutes using the application, this can quickly eliminate practically all loading times as the user gets to operate entirely against the local cache, which is updated with the result of mutations. And if some data needs to be fresher than that it’s easy to add
network-only
to a single query and refetch the minimum amount necessary.
With fragment composition, where each component is defining really granular fragments, not only is there more boilerplate (especially when refactoring and splitting up or merging components, oof) and basically every single developer needs to be really familiar with the exact structure of the GraphQL schema and tooling in order to do anything, but you lose most of the benefits of the normalized cache. If every screen is issuing a unique query with a unique combination of fragments, then chances are you’ll almost never get a cache hit and you’ll be fetching every fragment for every screen every time. I just don’t see how that’s desirable for any complex application where you can expect users to be doing many operations on the same underlying data over the course of a session.