Intended outcome:
writeFragment should update the cache.
Actual outcome:
When processing mutations using the update option I get unstable results. Writing to the cache isn't heavily documented, so I'm trying to understand the mechanism after days of trying to debug the issue.
My component's props method returns null for it's requested query item after it is mutated (not created -- just mutated). But only after the first mutation. If I refresh the page, this doesn't happen. And if I don't call update and just refresh the query it doesn't happen.
How to reproduce the issue:
I don't yet have a minimal repro app, but can repro it 100% in my current app.
The Project's HOC for it's query returns null (even though it is in the cache). I have verified that after the mutation I can call proxy.readFragment and retrieve the correct Item (and that it exist in the devtool's store). No network request is triggered -- since I presume Apollo "thinks" the item is cached.
After issuing the mutations (Project + Task) this is what happens:
1). mutate.update is called and I write an updated Task and Project to the cache.
2). the network request for the batch happens (and completes successfully).
3). 'Project.Query.options' is called twice (multiple requests always seem to happen but why -- redux chattiness?)
4). 'Project.Query.props' is called once with 'loading=false' and a null item.
5). 4 + 5 then happens again.
Even if I refresh the query after creating the fist mutations (i.e., new Task) then I get the same error when I change the Project and Task together. BUT if I refresh the query AFTER the second mutation the app is back in a functional state. IE this only happens for the first task that is created -- which is the same code path as subsequent mutations.
Hi @richburdon, this sounds like a very strange issue indeed. It looks like there's something there, but it's hard to debug based on the description, so I really hope you'll be able to put together a minimal reproduction somehow (ideally using react-apollo-error-template).
Hi @helfer, I'm still trying to break down a repro into a separate repo. I think I have a better guess at what is happening now (I have a workaround) -- so some questions:
1). I have a generic mutation that updates fields on Items. The mutation itself doesn't return any data from the server (just a success flag), but my custom mutate.update method can take the mutation values (optimistic or not), read the current Item value from the store (proxy), patch it (i.e., "apply" the mutation), then write it back to the store. This generally works fine.
However, say I have two components, A and B that have different queries (using different fragments) that query for the Item. Then in some cases the mutation "works" (i.e., seems to update the store -- as indicated by the Chrome extension) but the component is triggered with a null value for the Item.
After digging and digging it seems that if the mutation fragment doesn't _exactly_ match the query fragment then this error occurs (but only under special circumstances).
I had assumed that Items in the cache were merged when queries that have different fragments were written to the store; at least, this seems to happen by default. (If not, is there a way to control/coerce this?)
Example (again I'll try to follow up with this in a repo):
````
fragment ProjectFragment on Project {
id
title
tasks { # Array of task Items.
id
title
}
}
fragment ProjectExtraFragment on Project {
columns {
id
title
}
meta { # Array of metadata (e.g., task drag-drop ordering)
id
pos
}
}
query ProjectQuery($key: KeyInput!) {
item(key: $key) {
...ProjectFragment
...ProjectExtraFragment
}
${ProjectMutation}
${ProjectExtraFragment}
}
fragment ProjectMutation on Project {
...ProjectFragment
meta { # DIFFERENT SHAPE FROM QUERY FRAGMENTS.
id
pos
}
${ProjectMutation}
}
````
If I make sure the ProjectMutation is exactly the same as the fragments used in ProjectQuery then the problem goes away. If not then null is returned for the query under the following conditions:
i). Initially the Project returns the correct values.
ii). Inserting tasks works fine.
iii). But then adding a meta value (for the first time) fails (returns null when the query updates).
BUT: if I then restart, the initial query now returns meta values -- and adding subsequent values is fine.
So my wild guess is that there's some kind of cache consistency mechanism that determines if query results are compatible across different queries?
2). Related to my use case (but I think unrelated to the bug?) is an issue with mutate.update and writeFragment. If the mutation fragments are "overzealous" in declaring fields that "may" be updated and no value is provided, then there is a "Missing field" warning. So currently, I build a scaffold of null values for any missing fields (by introspecting the fragment definitions) before applying the actual mutation and writing to the store. Defaulting to null would be quite helpful here.
3). What is the best practice to test complex interactions? E.g., Setting up a component that has a query and mutation. I'm currently using jest and have a small testing state machine that is called whenever the component is rendered (e.g., post query, post mutation). I've looked at react-apollo-error-template but it seems to rely on manual testing? I'll follow-up with my mini-repo soon.
Hey @richburdon thanks for the detailed description. I don't have time to look into it, but I hope @jbaxleyiii will get to it at some point.
@richburdon thank you for the great description! It does seem like something funky around cache consistency is going on. There are a couple options for testing:
Since you are currently using components, I would recommend trying to reproduce via a small app in the testing portion of react-apollo. I would be happy to help fix it if you can reproduce it for us!
This issue has been automatically marked as stale becuase it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions to Apollo Client!
This issue has been automatically closed because it has not had recent activity after being marked as stale. If you belive this issue is still a problem or should be reopened, please reopen it! Thank you for your contributions to Apollo Client!
I believe I am having a similar issue and may have a good idea of the cause.
What I am seeing is that if you use writeFragment or writeQuery to update the store, and some components have a query that is affected by the store, BUT the data you have written is missing some field(s) needed to satisfy those components' query, they end up with no data and no network activity.
There is logic that refreshes all the component queries when the store is updated using writeQuery or writeFragment. As part of that it checks whether data is missing and sets a flag isMissing if so. When the data "disappears" what I see in the debugger is that isMissing is true and loading is false.
To elaborate, let us say we have two components A and B with these queries:
``
const queryA = gqlquery A {
widgets {
purpose
things {
use
lastUse
}
}
}`
queryB = gqlquery B {
widgets {
things {
use
}
}
}
Now, if I happen to use writeQuery(queryB, ...) to update the list of widgets, and there's no lastUse field provided in one of the things, queryA will report that data is missing, but never attempt to load that data. In some cases, it doesn't even report any errors.
+1 for @dobesv, I was stuck with my component throwing an error because of undefined data after updating with writeFragment but the new fragment had been correctly inserted in Apollo cache. It was simply dur to a field that was missing on the inserted fragment but the error is confusing.
BTW there's a fix in place now if you only use Query components you can specify an option refetchPartial and it'll automatically refetch if you update the cache in such a way that another query is missing some data.
Most helpful comment
I believe I am having a similar issue and may have a good idea of the cause.
What I am seeing is that if you use
writeFragmentorwriteQueryto update the store, and some components have a query that is affected by the store, BUT the data you have written is missing some field(s) needed to satisfy those components' query, they end up with no data and no network activity.There is logic that refreshes all the component queries when the store is updated using
writeQueryorwriteFragment. As part of that it checks whether data is missing and sets a flagisMissingif so. When the data "disappears" what I see in the debugger is thatisMissingistrueandloadingisfalse.To elaborate, let us say we have two components A and B with these queries:
``
const queryA = gqlquery A {widgets {
purpose
things {
use
lastUse
}
}
}`
queryB = gql
query B { widgets { things { use } } }Now, if I happen to use
writeQuery(queryB, ...)to update the list of widgets, and there's nolastUsefield provided in one of the things,queryAwill report that data is missing, but never attempt to load that data. In some cases, it doesn't even report any errors.