-
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
Reinstate cache.modify with improved API. #6350
Conversation
This reverts commit 61a799b.
This way, if a merge function happens to call cache.write recursively, it will not be broadcast in the middle of another write.
It's unlikely that the eviction will end up invoking any other cache update operations while it's running, but {in,de}crementing this.txCount still seems like a good/safe idea, if only for uniformity with the other update methods.
Apollo Client does not currently use this result internally, so alternate ApolloCache implementations are free to continue returning undefined.
We will definitely need more documentation about cache.modify after reverting PR #6289, but in the meantime the existing documentation should not be blatantly incorrect.
This distinction is a noteworthy benefit of using ModifyOptions rather than positional parameters for cache.modify.
That |
To my subjective taste, using a key called `fields` rather than `modifiers` makes it much easier to tell that "something" and "anything" are the names of fields: cache.modify({ id: cache.identify(...), fields: { something(value) { ... }, anything(value) { ... }, }, }) Since ModifyOptions is a new type introduced by PR #6350, we still have total freedom to improve the names of its fields.
public modify(options: ModifyOptions): boolean { | ||
if (hasOwn.call(options, "id") && !options.id) { | ||
// To my knowledge, TypeScript does not currently provide a way to | ||
// enforce that an optional property?:type must *not* be undefined | ||
// when present. That ability would be useful here, because we want | ||
// options.id to default to ROOT_QUERY only when no options.id was | ||
// provided. If the caller attempts to pass options.id with a | ||
// falsy/undefined value (perhaps because cache.identify failed), we | ||
// should not assume the goal was to modify the ROOT_QUERY object. | ||
// We could throw, but it seems natural to return false to indicate | ||
// that nothing was modified. | ||
return false; |
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.
One of the many benefits of using named options is that we can distinguish at runtime between options that were not provided at all and options that merely happen to have undefined values.
Awesome, I will definitely try this one so I'll we able to give some feedback on the other pending issues as well! |
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.
This looks awesome @benjamn - thanks for thinking all of this through so thoroughly and carefully! 🎉 The code changes all look great (I'll do some extra application testing very shortly and let you know how that all turns out). LGTM!
@darkbasic because I was having many issues when I tried to upgrade so I opt out for now but I will btw, in your example you are doing return {
...connection,
edges: connection.edges.filter(
({node}) => node.__ref !== deletedRef.__ref,
),
}; why don't you do this instead? return {
...connection,
edges: connection.edges.filter(
({node}) => readField("id", node) !== mutationData.removeMatch,
),
}; and you avoid using the |
@tafelito the main reason is because I wanted to showcase how ugly the |
By the way we've talked alot about how to improve the Reference experience for the upcoming v4 of Mikro-ORM, so if anybody is interested in the topic I suggest to have a look at mikro-orm/mikro-orm#145 and mikro-orm/mikro-orm#214 |
@darkbasic The Also, just so you know, |
Here's some background on why |
As I said I can be fine with the fact that const [removeMatch, {error: removeMatchError}] = useMutation<
RemoveMatchMutation,
RemoveMatchMutationVariables
>(RemoveMatchDocument, {
update(cache, {data}, {isOptimistic}) {
[...]
if (!isOptimistic) {
cache.evict([...]);
}
},
}); |
I kind of imagined this, but unfortunately it's still necessary to narrow the return type from |
Ok, The empty string thing is a bug that I will fix momentarily (in the next beta) by rewriting this code: public toReference = (
object: StoreObject,
mergeIntoStore?: boolean,
) => {
const [id] = this.policies.identify(object);
const ref = id && makeReference(id);
if (ref && mergeIntoStore) {
this.merge(id!, object);
}
return ref;
} to this: public toReference = (
object: StoreObject,
mergeIntoStore?: boolean,
) => {
const [id] = this.policies.identify(object);
if (id) {
const ref = makeReference(id);
if (mergeIntoStore) {
this.merge(id, object);
}
return ref;
}
} |
@tafelito That's an interesting question! Short answer: you definitely can call The
As for // XXX Not recommended; just an idea!
const tempRef = cache.writeFragment({
fragment: gql`fragment Temp on Temporary {
people {
firstName
lastName
}
}`,
data: {
__typename: "Temporary",
id: "???", // Not sure about the best fake ID to use here.
people: [
// Be sure that these objects all have __typename and id / key fields!
firstPerson,
secondPerson,
thirdPerson,
...
],
},
});
if (tempRef) {
const people = readField<Reference[]>("people", tempRef);
cache.evict(tempRef.__ref);
// Do something with people...
} I'm not ready to recommend this pattern in any way, but I imagine it would push the looping work down into the Alternatively, using // XXX Not recommended; just an idea!
const rootRef = cache.writeQuery({
// Be sure to use a root query field that isn't used elsewhere!
query: gql`query {
temporaryPeople {
firstName
lastName
}
}`,
data: {
temporaryPeople: [
// Be sure that these objects all have __typename and id / key fields!
firstPerson,
secondPerson,
thirdPerson,
...
],
},
});
const people = readField<Reference[]>("temporaryPeople", rootRef);
cache.evict({ fieldName: "temporaryPeople" });
// Do something with people... This works because |
I guess this would work, but it really looks ugly :) @benjamn the more I think about it the more I think that we should expose the query |
@benjamn I even started to think about stupid ways to work it around if that won't happen in time for Apollo 3.0, like returning the list of |
@darkbasic I think I agree about passing |
@benjamn I actually think that's a good idea. I like that the writeFragment would do the loop and I just have to pass the data to it |
Sure, and it's a lot less ugly if you hide it in a reusable helper function! |
One thing I noticed is that even if I modify the cache with either writeFragment or toReference on the updatefn, the original query still re run and fetches the server, I don't think that's supposed to happen, right? |
#6350 (comment) The modify method and its related types are fairly InMemoryCache-specific, which is why it felt safest to confine cache.modify to InMemoryCache when I brought it back in #6350. In practice, however, that confinement just makes cache.modify harder to use (annoyingly, not helpfully) in cases where an ApolloCache is all that's available. You could also argue that cache.modify has roughly the same standing (in terms of InMemoryCache-specificity) as cache.identify or cache.gc, which are also part of the "Optional API" of the ApolloCache base class. In that sense, cache.modify is simply joining those other AC3 APIs in ApolloCache.
Background: #6350 (comment) The modify method and its related types are fairly InMemoryCache-specific, which is why it felt safest to confine cache.modify to InMemoryCache when I brought it back in #6350. In practice, however, that confinement just makes cache.modify harder to use (annoyingly, not helpfully) in cases where an ApolloCache is all that's available. You could also argue that cache.modify has roughly the same standing (in terms of InMemoryCache-specificity) as cache.identify or cache.gc, which are also part of the "Optional API" of the ApolloCache base class. In that sense, cache.modify is simply joining those other AC3 APIs in ApolloCache (and thus also InMemoryCache).
@benjamn I think we also need a pointer to the current object (the same which ((cache as unknown) as InMemoryCache).modify({
id: cache.identify({
__typename: 'GroupChat',
id: chatOrGroupId,
}),
fields: {
messages: handleMessages,
},
});
((cache as unknown) as InMemoryCache).modify({
fields: {
messages: handleMessages,
},
}); In the previous example I will have to match the id from the cache.modify args for the |
For anyone still in need of something like |
@darkbasic Re: #6350 (comment), we are planning to add |
I know there's a lot to manage with this library, but could you pretty please with sugar on top update the docs. I'm trying to do the update with useMutation on @apollo/react-hooks v4.0.0 and I get the message... Here's the reference I'm talking about in the docs... |
Thanks to developer feedback (see #6289 (comment)) we have decided that
cache.modify
is an important piece of the API after all (at least for certain advanced use cases), and we believe it can be salvaged—but not without some tweaks. In addition to reverting PR #6289, this PR makes the following changes to improve thecache.modify
story:cache.modify
take named options rather than positional parameters. The positional parameter story was already showing its clumsiness in Make dataId parameter of cache.modify optional. #6178, and named options pave the way forcache.modify
to take options likeoptions.broadcast
(implemented forcache.write{Query,Fragment}
in Allow silencing broadcast for cache update methods. #6288).readField
helper to align with the newReadFieldOptions
API foroptions.readField
inread
andmerge
functions, as implemented in Allow calling readField with ReadFieldOptions. #6306. I skippedcache.modify
in that PR because I thought we were going to be removing it, but of coursereadField
should work the same everywhere.toReference
, and instead provide a pattern capable of handling the edge cases I described in Remove experimental cache.modify method. #6289 (comment). With this PR,cache.writeFragment
can be called withoutoptions.id
, allowing the cache to infer the ID fromoptions.data
. Callingcache.writeFragment
also returns aReference
to the object that was just written into the cache. With these improvements,cache.writeFragment
should be able to handle most of the use cases wheretoReference
was struggling, while also supporting field arguments, variables,merge
functions, etc. Best of all, thewriteFragment
API already exists, so the incremental documentation/explanation burden should be relatively small.cache.writeFragment
from withincache.modify
modifier functions, then we need to make sure all cache update methods can be safely called while other update operations are in progress. Specifically, callingcache.writeFragment
from within a modifier function should not trigger a broadcast in the middle of the largercache.modify
operation, because the larger operation will handle broadcasting when it finishes.options.cache
(theInMemoryCache
instance) toread
,merge
, andcache.modify
modifier functions. A small ergonomic improvement, but having access tooptions.cache
makes it easier to calloptions.cache.writeFragment
and other methods when you need them.Putting everything together:
@hwillson (and @stemmlerjs if you have time for a review): I definitely recommend a commit-by-commit approach to reviewing this PR.