-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Bail out of query update if data is undefined or same as previous data #2905
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
Conversation
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/tanstack/react-query/CBKueudpHjREPb43TRovHj8nsHS2 |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 9e77be1:
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 7d15c84:
|
|
||
// Notify cache callback | ||
this.cache.config.onSuccess?.(data, this as Query<any, any, any, any>) | ||
if (typeof updatedData !== 'undefined' && !Object.is(prevData, updatedData)) { |
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 would a polyfill for that, please see: https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/shared/objectIs.js
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.
ignore this if we go the route suggested in a later comment: #2905 (comment)
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 will add polyfill.
case 'ignore': | ||
return { | ||
...state, | ||
error: null, | ||
fetchFailureCount: 0, | ||
isFetching: false, | ||
isInvalidated: false, | ||
isPaused: false, | ||
status: typeof state.data === 'undefined' ? 'idle' : 'success', | ||
} |
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.
why do we need this please? Can we just not dispatch anything in case of bail-out and let the query be in that state as if setQueryData
was never called?
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 a query function returns undefined or the same data, we need to reset state after bailing out. Otherwise the query will stay in fetching state.
@@ -68,7 +68,7 @@ export class QueryObserver< | |||
TQueryKey | |||
> | |||
private previousQueryResult?: QueryObserverResult<TData, TError> | |||
private previousSelectError: Error | null | |||
private previousSelectError: TError | null |
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 change seems unrelated. Why is it necessary please?
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 are type errors in the latest version of Typescript. I can fix in separate PR if you prefer.
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.
yes lets please do this in a different branch, where we also update our TS version to the latest (4.5 as of a couple of minutes ago)
@@ -137,13 +137,13 @@ describe('queryClient', () => { | |||
test('should create a new query if query was not found', () => { | |||
const key = queryKey() | |||
queryClient.setQueryData(key, 'bar') | |||
expect(queryClient.getQueryData(key)).toBe('bar') | |||
expect(queryCache.find(key)).not.toBe(undefined) |
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.
why do we need to change that test - what was wrong with the previous assertion?
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.
getQueryData returns undefined for existing idle queries or non-existing queries. I wanted to ensure query was actually created.
|
||
function Page() { | ||
const state = useQuery(key, () => 'test', { | ||
const state = useQuery(key, () => queryReturnValue++, { |
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 test is named:
'should re-render when dataUpdatedAt changes but data remains the same'
now, data no longer stays the same. I was wondering if we're not breaking this functionality: Apparently, we would still expect dataUpdatedAt
to be updated if a new fetch comes in, which now likely doesn't happen because of the bail out...
sorry for the troubles, but I think the easiest solution would be to remove the Object.is
related bailouts and stick with the undefined
ones?
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.
Your instinct to combine undefined and same-data bailout was good, since the code changes happen in the same places. Let's stick with this. It is not hard to add code to update dataUpdatedAt. I just need to enhance the 'ignore' action handler to do this.
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.
Hm, I don't really think so. When I wrote this suggestion, I only had setQueryData
in mind, because that's where the issue stems from: That you cannot bail out of it even if you get undefined
passed in and can't create valid data.
I really think we should keep all changes confined to setQueryData
. If we fetch data, we can just put it in the cache without bailing out. structural sharing will make sure that data stays referentially stable, and RQ won't even re-render observers if the data doesn't change. So what's the point in bailing out here? I think it would also remove lots of code.
Additionally, we should in fact call the onSuccess
callback of observers that the fetch was successful, even if the data stays the same. I think bailing out here does more harm than good.
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.
The structural sharing takes place in Query.setData
, which is used by setQueryData
and fetching. So if we are going to give up on the Object.is bailout for fetching, we should do so for setQueryData as well.
Similarly, the call to onSuccess takes place in Query.setData
as well. So if we want to return onSuccess for fetching but not setQueryData, it would require some kind of flag, which is smelly.
Since these changes are for React Query 4, I am going to venture my opinion. A large part of the confusion here is that we are using onSuccess
in two very different ways. We use it to indicate a successful fetch, and we use it to indicate a data change. Instead, we need two different callbacks, onSuccess
when a fetch succeeds and onDataChanged
whenever cache is updated with new data via setQueryData
or the fetch. I can move this discussion to Discussions if that seems more appropriate.
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.
The structural sharing takes place in Query.setData, which is used by setQueryData and fetching.
this is what I meant when I said we'd need some internal restructurings. calling the functional updater + performing structural sharing / isDataEqual check can be a pure function - it only depends on data
and options
- both of which we have available. This only leaves us with
// Set data and mark it as cached
this.dispatch({
data,
type: 'success',
dataUpdatedAt: options?.updatedAt,
})
that we would need to somehow put into a function on the query, like setSuccessData
and call it in two places: from query.setData
and from queryClient.setQueryData
, where we could only do it after we've computed the new data by calling the aforementioned function, so potentially, not calling query.setData
from queryClient.setQueryData
, thus keeping the separation between "setting data after fetching" and "setting data from setQueryData"
i like the idea of an extra callback - it was confusing to ppl that onSuccess
is also called after setQueryData
, but it's a different topic I think
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, this all makes sense. I will get to work on it.
If we ever split out onDataChanged and onSuccess, we will need to do the kind of code split you are proposing above anyway. I will propose this change that in a discussion.
Only one issue to resolve: what do we do if the query function returns undefined? If we are not bailing out, I suggest we treat this as an error.
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.
Proposed onDataChanged callback: #2911
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.
Only one issue to resolve: what do we do if the query function returns undefined? If we are not bailing out, I suggest we treat this as an error.
this is also what Tanner proposed yesterday for v4: we throw an error and put the query in error state. In development mode, we could additionally warn, and if possible, we should exclude undefined on type level :)
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.
Sounds good, I will make this change.
@@ -143,7 +144,7 @@ describe('queryObserver', () => { | |||
await sleep(1) | |||
await observer.refetch() | |||
unsubscribe() | |||
expect(count).toBe(2) | |||
expect(count).toBe(3) |
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.
why did this change please? not sure why this test needed to change at all :/
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 the query function returned a constant value, then only the first call would have any effect because of the bail out change. So now it returns a different value on each call. This results in one extra call to select
.
I've created an issue for this here: #2925 and assigned you to it. /edit: ok, I can't assign you to it 🙈 please also change the base of your PR to the |
@phatmann any updates here please? |
Sorry for the activity gap. I'm preparing an app release for my employer and that is sucking up my time. I'll be back on this in about a week. |
I should finally have time soon to get back to this. I have not forgotten about it! |
@phatmann hope you're having a great new year. Let me know if I can help you with the PR :) |
Still waiting for that magical moment when my new app release has landed with no outstanding issues. Really hoping that is any day now. And then I can get back to this PR. |
@phatmann since we are aiming for a release soon, I would really like to get this over the finish line. If I'm not hearing back from you, I will close the PR and start a new one. |
@TkDodo I will work on it today! |
@TkDodo after reviewing the comments, I see that we will be better off making a new (narrower) PR and closing this one. So I will do that. Expect a new PR today. |
Opened #3271 |
See #2823
closes #2925