-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support toReference(obj, true) to persist obj into cache. #5970
Conversation
The options.toReference helper function is provided to custom read and merge functions, as well as to cache.modify modifier functions, via their details parameter (see #5909). See the tests included in this commit for examples of all three usages. The original purpose of toReference was to return a Reference object, { __ref: <ID> }, given an object with a __typename and the necessary key fields for that type. If these expectations are not met, toReference(obj) can return null, to indicate that the object could not be identified. Another toReference failure mode (besides returning null) is returning a Reference object that doesn't actually refer to anything stored in the cache. While this case is handled gracefully while reading from the cache (the non-existent object simply appears empty), you may have encountered cases where you _do_ want to persist the object that was passed to toReference into the cache, such as when a custom read function knows how to provide a perfectly adequate default object based on options.args. For those use cases, you can now call toReference(object, true) to merge the fields of the object into the cache, assuming it could be identified. The fields of the object are merged into the cache as-is, without any transformations to handle field arguments, aliases, directives, or variables. For those cases, cache.writeQuery or cache.writeFragment are still the best options. Also, toReference is not exposed outside of read/merge/modify functions, so if you're not in one of those functions you will have to call the normal writeQuery or writeFragment methods. Implementation-wise, since toReference may now need to call store.merge, it was most convenient to move the definition of the toReference method from the Policies class to the EntityStore class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great @benjamn - this will definitely come in handy!
Codecov Report
@@ Coverage Diff @@
## master #5970 +/- ##
==========================================
+ Coverage 95.04% 95.06% +0.02%
==========================================
Files 90 90
Lines 3710 3711 +1
Branches 881 881
==========================================
+ Hits 3526 3528 +2
+ Misses 161 160 -1
Partials 23 23 Continue to review full report at Codecov.
|
The `cache.modify` API was first introduced in #5909 as a quick way to transform the values of specific existing fields in the cache. At the time, `cache.modify` seemed promising as an alternative to the `readQuery`-transform-`writeQuery` pattern, but feedback has been mixed, most importantly from our developer experience team, who helped me understand why `cache.modify` would be difficult to learn and to teach. While my refactoring in #6221 addressed concerns about broadcasting `cache.modify` updates automatically, the bigger problem with `cache.modify` is simply that it requires knowledge of the internal workings of the cache, making it nearly impossible to explain to developers who are not already Apollo Client 3 experts. As much as I wanted `cache.modify` to be a selling point for Apollo Client 3, it simply wasn't safe to use without a firm understanding of concepts like cache normalization, references, field identity, read/merge functions and their options API, and the `options.toReference(object, true)` helper function (#5970). By contrast, the `readQuery`-transform-`writeQuery` pattern may be a bit more verbose, but it has none of these problems, because these older methods work in terms of GraphQL result objects, rather than exposing the internal data format of the `EntityStore`. Since `cache.modify` was motivated to some extent by vague power-user performance concerns, it's worth noting that we recently made `writeQuery` and `writeFragment` even more efficient when rewriting unchanged results (#6274), so whatever performance gap there might have been between `cache.modify` and `readQuery`/`writeQuery` should now be even less noticeable. One final reason that `cache.modify` seemed like a good idea was that custom `merge` functions can interfere with certain `writeQuery` patterns, such as deleting an item from a paginated list. If you run into trouble with `merge` functions running when you don't want them to, we recommend calling `cache.evict` before `cache.writeQuery` to ensure your `merge` function won't be confused by existing field data when you write your modified data back into the cache.
The `cache.modify` API was first introduced in #5909 as a quick way to transform the values of specific existing fields in the cache. At the time, `cache.modify` seemed promising as an alternative to the `readQuery`-transform-`writeQuery` pattern, but feedback has been mixed, most importantly from our developer experience team, who helped me understand why `cache.modify` would be difficult to learn and to teach. While my refactoring in #6221 addressed concerns about broadcasting `cache.modify` updates automatically, the bigger problem with `cache.modify` is simply that it requires knowledge of the internal workings of the cache, making it nearly impossible to explain to developers who are not already Apollo Client 3 experts. As much as I wanted `cache.modify` to be a selling point for Apollo Client 3, it simply wasn't safe to use without a firm understanding of concepts like cache normalization, references, field identity, read/merge functions and their options API, and the `options.toReference(object, true)` helper function (#5970). By contrast, the `readQuery`-transform-`writeQuery` pattern may be a bit more verbose, but it has none of these problems, because these older methods work in terms of GraphQL result objects, rather than exposing the internal data format of the `EntityStore`. Since `cache.modify` was motivated to some extent by vague power-user performance concerns, it's worth noting that we recently made `writeQuery` and `writeFragment` even more efficient when rewriting unchanged results (#6274), so whatever performance gap there might have been between `cache.modify` and `readQuery`/`writeQuery` should now be even less noticeable. One final reason that `cache.modify` seemed like a good idea was that custom `merge` functions can interfere with certain `writeQuery` patterns, such as deleting an item from a paginated list. If you run into trouble with `merge` functions running when you don't want them to, we recommend calling `cache.evict` before `cache.writeQuery` to ensure your `merge` function won't be confused by existing field data when you write your modified data back into the cache.
The `cache.modify` API was first introduced in #5909 as a quick way to transform the values of specific existing fields in the cache. At the time, `cache.modify` seemed promising as an alternative to the `readQuery`-transform-`writeQuery` pattern, but feedback has been mixed, most importantly from our developer experience team, who helped me understand why `cache.modify` would be difficult to learn and to teach. While my refactoring in #6221 addressed concerns about broadcasting `cache.modify` updates automatically, the bigger problem with `cache.modify` is simply that it requires knowledge of the internal workings of the cache, making it nearly impossible to explain to developers who are not already Apollo Client 3 experts. As much as I wanted `cache.modify` to be a selling point for Apollo Client 3, it simply wasn't safe to use without a firm understanding of concepts like cache normalization, references, field identity, read/merge functions and their options API, and the `options.toReference(object, true)` helper function (#5970). By contrast, the `readQuery`-transform-`writeQuery` pattern may be a bit more verbose, but it has none of these problems, because these older methods work in terms of GraphQL result objects, rather than exposing the internal data format of the `EntityStore`. Since cache.modify was motivated to some extent by vague power-user performance concerns, it's worth noting that we recently made writeQuery and writeFragment even more efficient when rewriting unchanged results (#6274), so whatever performance gap there might have been between cache.modify and readQuery/writeQuery should now be even less noticeable. One final reason that `cache.modify` seemed like a good idea was that custom `merge` functions can interfere with certain `writeQuery` patterns, such as deleting an item from a paginated list. If you run into trouble with `merge` functions running when you don't want them to, we recommend calling `cache.evict` before `cache.writeQuery` to ensure your `merge` function won't be confused by existing field data when you write your modified data back into the cache.
The
options.toReference
helper function is provided to customread
andmerge
functions via theiroptions
parameter, as well as tocache.modify
modifier functions, via theirdetails
parameter (#5909). See the tests included in this commit for examples of all three usages.The original purpose of
toReference
was to return aReference
object,{ __ref: <entity ID> }
, given an object with a__typename
and the necessary key fields for that type. If these expectations are not met,toReference(obj)
can returnnull
, to indicate that the object could not be identified.Another
toReference
failure mode (besides returningnull
) is returning aReference
object that doesn't actually refer to anything stored in the cache. While this case is handled gracefully during cache reads (the non-existent object simply appears empty), you may have encountered cases where you do want to persist the object that was passed totoReference
into the cache, such as when a customread
function knows how to provide a perfectly adequate default object based onoptions.args
.For those use cases, you can now call
toReference(object, true)
to merge the fields of the object into the cache, assuming it could be identified. Because this merging uses the sameEntityStore#merge
method as any other cache update, any dependent queries will be appropriately invalidated.The fields of the object are merged into the cache as-is, without any transformations to handle field arguments, aliases, directives, or variables. For those cases,
cache.writeQuery
orcache.writeFragment
are still the best options. Also,toReference
is not exposed outside ofread
/merge
/modify
functions, so if you're not in one of those functions you will have to call the normalwriteQuery
orwriteFragment
methods.Implementation-wise, since
toReference
may now need to callstore.merge
, it was most convenient to move the definition of thetoReference
method from thePolicies
class to theEntityStore
class.