After seeing this code would you still recommend the switch? Does Apollo handle fine-grained rerenders better than GraphQL’s implementation?
Yes! Mostly due to the performance front. At the very least, I’m glad you’re using the fragment colocation pattern
. We are making more of a push to make this the default way to write apps with Apollo Client.
GraphQL Codegen’s useFragment
“hook” (its actually just a function, not a hook) is purely a TypeScript helper function. At runtime, its just an identity function. Here is the output of useFragment
when I run it in the spotify showcase repo:
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
): TType | undefined;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentTypeDecoration<TType, any>>
| null
| undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
): Array<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined
): Array<TType> | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return readonly array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentTypeDecoration<TType, any>>
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
You’ll see this is just a bunch of function overloads that handle various TypeScript types. This is the key part of the runtime though:
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentTypeDecoration<TType, any>>
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
Notice it just returns the input unmodified so its just an identity function. This is why I put “hook” in quotes and mentioned its just a function (as I’m typing this, I realized you changed the name to getFragmentData
which is fantastic so you don’t confuse it with a hook
).
Since its just an identity function, this means that updates to data in those fragments will render the query component as well. This is probably fine for most cases, but it means you’re re-rendering components that don’t use or consume that data and are just passing that data down to child components.
Apollo’s useFragment
hook (combined with data masking) instead won’t rerender your query component and will only rerender the useFragment
components that are affected by that change. Thats going to be the biggest difference in performance.
That said, I’d be remiss if I didn’t mention a few other differences between the two. Apollo’s useFragment
hook has a few limitations due to how it works at runtime that otherwise “just work” with GraphQL Codegen’s useFragment
function since its just an identity function.
- You can’t use arrays currently with
useFragment
(meaning the from
option only accepts a single record). We’ll be addressing this in a 4.x release 
- The parent MUST select any keyFields available to the record you’re providing to
useFragment
. useFragment
is technically a cache API so it requires that it can identify the object in the cache. This also means:
- You can’t currently use it with non-normalized data
- You can’t use it with
no-cache
queries
If you can, I’d recommend using Apollo’s useFragment
hook moving forward and try to migrate over time away from codgen’s fragment masking feature. Almost all of the same patterns apply between GraphQL Codegen and Apollo Client with the way you’re already using it (with the exception of the things I mentioned above). The biggest difference will be the function signature between the two.
I forgot to mention this as well, but I’d recommend reading this blog post by Alessia Bellesario (former Apollo Client mantainer) on the useFragment
hook which does a great job showcasing the performance differences: Don’t Overreact! Introducing Apollo Client’s new @nonreactive directive and useFragment hook | Apollo GraphQL Blog
Note that article was written before data masking existed. You can think of data masking like automatically applying that @nonreactive
directive everywhere a fragment spread is used. But @nonreactive
might be helpful if you decide to start moving toward Apollo Client’s useFragment
hook and still want some of the perf benefits without doing a full migration.
If data masking interests you, we have a full incremental migration guide that walks you through how to adopt it.
Does this help?