refactor useQuery to not use an internal class#11869
Conversation
🦋 Changeset detectedLatest commit: 33c0fef The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
size-limit report 📦
|
✅ Deploy Preview for apollo-client-docs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
|
I've done a commit-by-commit look through this PR, but I'd like to wait to fully comment on this until you've had a chance to merge the base branches into this one. I think this contains changes from a couple other PRs, so its difficult to tell whats from that and whats from the other branches. Once that is done, I should be able to get a quick review submitted. Thanks! |
ec7621f to
a8f9e70
Compare
|
Did a bunch of cherry-picking and history rewriting - it should be good now! |
a8f9e70 to
a69327c
Compare
There was a problem hiding this comment.
I still find a good portion of this fairly awkward but I think that's less a fault of this refactor and more that this refactor really illuminated some of the already tight coupling in a lot of the areas of the hook that were there as a result of the class usage. As much as I'd love to continue refactoring into oblivion, we gotta ship something and I think this is a huge improvement over what we've had.
I've added quite a few suggestions, but many of them are non-blocking so I'll let you use your best judgement on whether it makes sense to change them or not.
|
|
||
| */ | ||
| expect(obsQueries.size).toBe(IS_REACT_19 ? 1 : 2); | ||
| expect(obsQueries.size).toBe(2); |
| @@ -1,19 +1,38 @@ | |||
| /** | |||
| * Function parameters in this file try to follow a common order for the sake of | |||
There was a problem hiding this comment.
I'm curious, why this approach vs using an object to pass in values to each of the functions? There are cases such as this one that I would find easier to reason about with the object syntax vs positional args.
getObsQueryOptions(
undefined,
client,
options,
makeWatchQueryOptions()
)
// =>
getObsQueryOptions({
client,
options,
watchQueryOptions: makeWatchQueryOptions()
})I fear these rules will be too easy to break in the future and if we need to add more arguments it will be difficult to what the "right" order would be.
There was a problem hiding this comment.
Positional function arguments can be minimized, while option object property names cannot.
In the end, you end up with
getObsQueryOptions(
void 0,
c,
o,
makeWatchQueryOptions()
)vs
getObsQueryOptions({
client: c,
options: o,
watchQueryOptions: makeWatchQueryOptions()
})and the same difference on the declaration side, too:
function getObsQueryOptions(a,b,c,d){}vs
function getObsQueryOptions({
observable: a,
client: b,
options: c,
watchQueryOptions: d
}){}or probably transpiled to
function getObsQueryOptions(o){
var a=o.observable, b=o.client, c=o.options, d=o.watchQueryOptions
}This adds up, a lot.
With a file like this, which contains a lot of helper hooks, we could probably in the long run even use something like the Clojure compiler that would inline a bunch of the hooks - but only with positional arguments, not with config objects.
I fear these rules will be too easy to break in the future and if we need to add more arguments it will be difficult to what the "right" order would be.
Tbh., I'm fine with that. I wanted to get this in as orderly as possible for now (because during refactoring, it was a mess), but if things evolve, that's okay too :)
| renderPromises.getSSRObservable(makeWatchQueryOptions())) || | ||
| client.watchQuery( | ||
| getObsQueryOptions( |
There was a problem hiding this comment.
| renderPromises.getSSRObservable(makeWatchQueryOptions())) || | |
| client.watchQuery( | |
| getObsQueryOptions( | |
| renderPromises.getSSRObservable(makeWatchQueryOptions())) || | |
| client.watchQuery( | |
| getObsQueryOptions( |
I know this is a result of keeping much of the code the same, but something I'm noticing here is that both makeWatchQueryOptions and getObsQueryOptions both return WatchQueryOptions. I find it very interesting that we sorta use them both together.
I'm not sure if there is any room for any further refactoring here, but it would be lovely if we could have a single place where we handle the creation of watch query options. I find it a bit difficult to understand when we'd want to use one vs the other.
Feel free to push back on this request, but I figured I'd note it as something that I found a bit confusing.
There was a problem hiding this comment.
I think the name getObsQueryOptions is just poorly chosen.
It's more getWatchQueryOptionsPotentiallyMergingSomeOptionsFromThePreviousObservable.
In the getSSRObservable case, there is just a guarantee of "observable will never change", so the call is not necessary there.
I'd say let's leave it for now, but it is something I hope we can get rid of in the long term altogether.
| options: QueryHookOptions<NoInfer<TData>, NoInfer<TVariables>>, | ||
| watchQueryOptions: Readonly<WatchQueryOptions<TVariables, TData>> | ||
| ) { | ||
| { |
There was a problem hiding this comment.
Are these braces needed? Any chance we could remove the { } surrounding the implementation here?
There was a problem hiding this comment.
Uhh... yah no idea how I got these there..
| ); | ||
| export function toQueryResult<TData, TVariables extends OperationVariables>( | ||
| result: ApolloQueryResult<TData>, | ||
| resultData: InternalResult<TData, TVariables>, |
There was a problem hiding this comment.
| resultData: InternalResult<TData, TVariables>, | |
| previousData: TData | undefined, |
Could we pass in previousData here instead? The resultData.current = toQueryResult(resultData) feels a tad odd because I'd expect to mutate resultData or something else. Changing this to previousData makes it more obvious what the dependency is here.
| const queryResult: InternalQueryResult<TData, TVariables> = { | ||
| data, // Ensure always defined, even if result.data is missing. | ||
| ...resultWithoutPartial, | ||
| client: client, |
There was a problem hiding this comment.
I wonder if there is an opportunity to do a touch more with the value returned from this function. I see that client is set here, which appears to be used by _useQuery (via result), but I also see that useQueryInternals returns a client as well, which should be the same object. Seems we have some overlap between result and the return value of useQueryInternals. Any chance we could reduce the overlap between the two a bit further? Perhaps client and observable can be read from result instead of the top-level object in useQueryInternals.
There was a problem hiding this comment.
I just gave that a try - it adds another 15 bytes (this touches a lot of code), and I'm not 100% sure without a very thorough review if this would always hold the right client for the right request.
As this whole "result handling" logic is something I kinda want to revamp anyways, I'd also say let's shelve this one.
| (handleStoreChange) => { | ||
| // reference `disableNetworkFetches` here to ensure that the rules of hooks | ||
| // keep it as a dependency of this effect, even though it's not used | ||
| disableNetworkFetches; |
There was a problem hiding this comment.
Side note:
At some point, it would be great to write a test for this. I've commented this out here and from the dependency array and everything continues to pass in both the main and ssr useQuery tests. Are we sure this reference is actually needed? Might be good to evaluate if this is actually covered by a test somewhere (sorry I'm not doing that deep dive myself right now 😆)
There was a problem hiding this comment.
I'm pretty sure it's not covered by a test 😭
This is a very weird workaround for SSR that we should also get rid of at some point.
c903b43 to
dfda94e
Compare
Co-authored-by: Jerel Miller <jerelmiller@gmail.com>
dfda94e to
987aaad
Compare
8be4f63 to
33c0fef
Compare
After #11890, I'm fairly confident in this now.
This needs to be reviewed commit-by commit, although a bunch of merges added additional commits at the start and end - I'm really sorry for those! (I believe some of them will disappear here if #11890 gets merged first!)
There might be more PRs to further streamline the result handling, but I think this PR is big enough as it is right now.