-
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
Preserve referential equality from previous result in the store #1136
Preserve referential equality from previous result in the store #1136
Conversation
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.
Minor comments
// if this is an object scalar, it must be a json blob and we have to unescape it | ||
// If the JSON blob is the same now as in the previous result, return the previous result to | ||
// maintain referential equality. | ||
if (idValue.previousResult && isEqual(idValue.previousResult[fieldName], fieldValue.json)) { |
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.
I think we can use === here instead of isEqual
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.
Test fails when I do that. It’s because two requests will never return the exact same object (when compared with ===
), so we have to do a deep equals.
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.
OK I'm fine with this for now - let's do some benchmarks later and see if it is ever a problem!
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.
I'm not sure I follow here. Surely once written to the store the object doesn't change, right? It's only when another request fetches the same object that the reference changes. If that's the case, then we could first check referential equality and only if that fails apply isEqual
.
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.
@helfer isEqual
always does a referential check first. If you’d like we could do another one here to make the intention explicit.
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.
If you don’t care about referential equality when a new JSON object comes back from the API (which when a deep comparison is performed is actually the same object) we could just remove this entirely as the edge case may not be worth supporting at all. I only really added it to get this test to pass, otherwise it’s a safe removal.
// | ||
// While we do a shallow comparison of objects, we do a deep comparison of arrays. | ||
const sameAsPreviousResult = Object.keys(resultFields).reduce( | ||
(same, key) => { |
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.
Code style - I'd put these args on the previous line.
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.
Yeah, this line went through multiple iterations.
// Flatten out the field values before comparing them. Non-arrays will turn into singleton | ||
// arrays and multi-dimensional arrays will be flattened out. Depth doesn’t matter in this | ||
// case, we just need to check that all items are equal. | ||
const next = flattenArray(resultFields[key]); |
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 would definitely pass through a false positive if the arrays had the same items in flattened form but had different shapes.
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.
You’re right. I initially patched isEqual
to do shallow object checks when a config flag was passed, but that felt wrong so I changed it to this. There is probably a better solution.
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 is really awesome! 👍 I'm especially liking the thorough yet relatively succinct and clean tests.
I agree that it may feel a bit contrived, but it fits the graphql-anywhere pattern and I don't see how we could improve much on it while still using graphql-anywhere. I believe it will take a moment to understand what's going on for folks who aren't familiar with graphql-anywhere (eg. practically everyone outside the apollo team) so I do think it would help to add a comment at the top that explains how the resolver and resultMapper play together to make things work.
Actually, that reminds me that we should still write a blog post at some time that explains what graphql-anywhere is and does!
// Flatten out the field values before comparing them. Non-arrays will turn into singleton | ||
// arrays and multi-dimensional arrays will be flattened out. Depth doesn’t matter in this | ||
// case, we just need to check that all items are equal. | ||
const next = flattenArray(resultFields[key]); |
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.
What are the performance implications of this? I'm not too worried about deeply nested arrays at this point in time, but what about one-dimensional arrays? If we're copying every array every time to make a comparison that could amount to a fair bit of copying.
Although... I once said never optimize something you don't already know needs optimizing. So that might count here 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.
My guess would be it isn’t a huge deal, but I’m going to change it anyway. @stubailo pointed out it could be a source of bugs by creating false positives.
// if this is an object scalar, it must be a json blob and we have to unescape it | ||
// If the JSON blob is the same now as in the previous result, return the previous result to | ||
// maintain referential equality. | ||
if (idValue.previousResult && isEqual(idValue.previousResult[fieldName], fieldValue.json)) { |
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.
I'm not sure I follow here. Surely once written to the store the object doesn't change, right? It's only when another request fetches the same object that the reference changes. If that's the case, then we could first check referential equality and only if that fails apply isEqual
.
previousResult, | ||
}); | ||
|
||
assert.deepEqual(result, queryResult); |
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.
I guess this assertion wouldn't strictly be necessary, but you put it in to speed up debugging if things are still deepEqual
but not strictly equal?
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.
Yeah, it’s more of a quick sanity check then anything else. For instance result
might not deep equal queryResult
if we wrote result.a.b = 10
on line 432 but would still strict equal previousResult
.
@@ -346,4 +347,347 @@ describe('diffing queries against the store', () => { | |||
}); | |||
}); | |||
}); | |||
|
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.
These tests look really neat! For some reason that makes me really happy.
@helfer when I talked to Caleb he mentioned that |
Added some commits to address comments and improve file legibility. I’m going to work on getting the |
@@ -15,6 +15,7 @@ Expect active development and potentially significant breaking changes in the `0 | |||
- Remove lodash as a production dependency [PR #1122](https://github.com/apollostack/apollo-client/pull/1122) | |||
- Minor fix to write to `ROOT_SUBSCRIPTION` ID in the store for subscription results. [PR #1122](https://github.com/apollostack/apollo-client/pull/1127) | |||
- Remove `whatwg-fetch` polyfill dependency and instead warn when a global `fetch` implementation is not found. [PR #1134](https://github.com/apollostack/apollo-client/pull/1134) | |||
- New results from `watchQuery` are referentially equal (so `a === b`) to previous results if nothing changed in the store for a better UI integration experience when determining what changed. [PR #1136](https://github.com/apollostack/apollo-client/pull/1136) |
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 misses the important point that this applies to sub-objects in the result, not just the whole result (which was true before!)
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.
Fixed 👍
We should write a blog post about this when it ships. I think it will do a lot to demonstrate the attention to detail of what we're doing. |
} | ||
// Otherwise let us compare all of the array items (which are potentially nested arrays!) to see | ||
// if they are equal. | ||
return a.reduce((same, item, i) => same && areNestedArrayItemsStrictlyEqual(item, b[i]), true); |
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.
With this comparison we'd end up with [1,2] === [1,2,3]
because we're only comparing up to the number of items in array a.
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.
Yep, that’s my bad. Comparing length up front is probably good hygiene anyway.
Sweet, that's an awesome PR @calebmer! I added Going to merge this now. |
WUAT? This bugged me for a long time. Awesome! Thank you very much and sorry for the spam... just wanted to show my gratitude! :D |
This PR implements the preservation of referential equality across reads from the store for data that has not changed. So say you have an array of
[a, b, c]
and a mutation adds a new item,d
, to make[a, b, c, d]
. Then the following should holdlast[0] === next[0] && last[1] === next[1] && last[2] === next[2]
while the array itself is not necessarily referentially equal (last !== next
). This makes it easier to create optimizations in the UI layer to only re-render data which changed. Specifically this would make the implementation ofshouldComponentUpdate
in React apps much cleaner.This PR is not yet finished, but I’d like an initial review of what’s there. I’m going to work on some other issues for a bit then come back to this. The implementation is a little messy as the
graphql-anywhere
resolver andresultMapper
need to work together to both identify which values have previous results and if those previous results equal the new result. The implementation gets a little cleaner if we remove support for arrays whose order changes (e.g.[a, b, c] -> [b, c, a]
), but that means we wouldn’t support the case where an item is added to the beginning of an array which is pretty important (e.g.[a, b, c] -> [d, a, b, c]
).What’s left to do is wiring this up with wherever it is called so that
previousResult
values actually get passed down intodiffQueryAgainstStore
.Closes #695