From 36060be9f2a2e5d6f3d69a9cbe48d353aeee413f Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 29 Dec 2023 12:45:19 +0100 Subject: [PATCH 1/3] fix(react): don't set options before suspending setting options right before suspending is a side effect, and it has repercussions if used in transitions where the component stays mounted to fix #6392 in a different way, we can't rely on `observer.getCurrentQuery()` when reading for error boundaries / suspense, because it can lag behind; instead, we can just read from the queryCache directly to get the latest values --- packages/query-core/src/types.ts | 4 +-- .../react-query/src/errorBoundaryUtils.ts | 3 ++- packages/react-query/src/useBaseQuery.ts | 7 +++-- packages/react-query/src/useQueries.ts | 27 +++++++++---------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index f0cd75363c..4f951a3de9 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -345,7 +345,7 @@ export type DefaultedQueryObserverOptions< TQueryKey extends QueryKey = QueryKey, > = WithRequired< QueryObserverOptions, - 'throwOnError' | 'refetchOnReconnect' + 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > export interface InfiniteQueryObserverOptions< @@ -381,7 +381,7 @@ export type DefaultedInfiniteQueryObserverOptions< TQueryKey, TPageParam >, - 'throwOnError' | 'refetchOnReconnect' + 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > export interface FetchQueryOptions< diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index d46784341b..7721812058 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -57,12 +57,13 @@ export const getHasError = < result: QueryObserverResult errorResetBoundary: QueryErrorResetBoundaryValue throwOnError: ThrowOnError - query: Query + query: Query | undefined }) => { return ( result.isError && !errorResetBoundary.isReset() && !result.isFetching && + query && shouldThrowError(throwOnError, [result.error, query]) ) } diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index b422a70b43..2b295860ce 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -94,7 +94,6 @@ export function useBaseQuery< // Do the same thing as the effect right above because the effect won't run // when we suspend but also, the component won't re-mount so our observer would // be out of date. - observer.setOptions(defaultedOptions, { listeners: false }) throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary) } @@ -104,7 +103,11 @@ export function useBaseQuery< result, errorResetBoundary, throwOnError: defaultedOptions.throwOnError, - query: observer.getCurrentQuery(), + query: client + .getQueryCache() + .get( + defaultedOptions.queryHash, + ), }) ) { throw result.error diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 1097cc2969..b2c000d922 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -329,24 +329,21 @@ export function useQueries< : [] if (suspensePromises.length > 0) { - observer.setQueries( - defaultedQueries, - options as QueriesObserverOptions, - { - listeners: false, - }, - ) throw Promise.all(suspensePromises) } - const observerQueries = observer.getQueries() const firstSingleResultWhichShouldThrow = optimisticResult.find( - (result, index) => - getHasError({ - result, - errorResetBoundary, - throwOnError: defaultedQueries[index]?.throwOnError ?? false, - query: observerQueries[index]!, - }), + (result, index) => { + const query = defaultedQueries[index] + return ( + query && + getHasError({ + result, + errorResetBoundary, + throwOnError: query.throwOnError, + query: client.getQueryCache().get(query.queryHash), + }) + ) + }, ) if (firstSingleResultWhichShouldThrow?.error) { From 0b33cd80004f59f1a63d5db622e895e34527f7ed Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 29 Dec 2023 18:16:26 +0100 Subject: [PATCH 2/3] chore: fix prettier --- packages/react-query/src/useBaseQuery.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index 2b295860ce..4a2e3fc6bb 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -105,9 +105,12 @@ export function useBaseQuery< throwOnError: defaultedOptions.throwOnError, query: client .getQueryCache() - .get( - defaultedOptions.queryHash, - ), + .get< + TQueryFnData, + TError, + TQueryData, + TQueryKey + >(defaultedOptions.queryHash), }) ) { throw result.error From a2f75bf164a243f8916492e9b2514c5a8b616f6b Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 29 Dec 2023 22:29:49 +0100 Subject: [PATCH 3/3] chore: test for 6486 --- .../src/__tests__/suspense.test.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/react-query/src/__tests__/suspense.test.tsx b/packages/react-query/src/__tests__/suspense.test.tsx index 5bdbb9c0b5..dc689913ee 100644 --- a/packages/react-query/src/__tests__/suspense.test.tsx +++ b/packages/react-query/src/__tests__/suspense.test.tsx @@ -1033,4 +1033,61 @@ describe('useSuspenseQueries', () => { await waitFor(() => rendered.getByText('data1')) }) + + it('should not request old data inside transitions (issue #6486)', async () => { + const key = queryKey() + let queryFnCount = 0 + + function App() { + const [count, setCount] = React.useState(0) + + return ( +
+ + + + +
+ ) + } + + function Page({ count }: { count: number }) { + const { data } = useSuspenseQuery({ + queryKey: [key, count], + queryFn: async () => { + queryFnCount++ + await sleep(10) + return 'data' + count + }, + }) + + return ( +
+
{String(data)}
+
+ ) + } + + const rendered = renderWithClient( + queryClient, + + , + ) + + await waitFor(() => rendered.getByText('Loading...')) + + await waitFor(() => rendered.getByText('data0')) + + fireEvent.click(rendered.getByText('inc')) + + await waitFor(() => rendered.getByText('data1')) + + await sleep(20) + + expect(queryFnCount).toBe(2) + }) })