From 4d56163c464a3f1ace6fa9eaa4d8f83579c52c2f Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 26 Jul 2024 11:49:04 +0200 Subject: [PATCH 1/3] fix(react-query): make sure that we never render with undefined data when using suspense We guarantee this on type level, but at runtime, if `select` throws an error, the defaultThrowOnError would return `false` because data is only undefined on observer level. The extra check for observer data in suspense mode guarantees this --- .../src/__tests__/suspense.test.tsx | 45 ++++++++++++++++++- .../react-query/src/errorBoundaryUtils.ts | 5 +++ packages/react-query/src/useBaseQuery.ts | 1 + packages/react-query/src/useQueries.ts | 1 + 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/react-query/src/__tests__/suspense.test.tsx b/packages/react-query/src/__tests__/suspense.test.tsx index f13164b1b0..203a89e02d 100644 --- a/packages/react-query/src/__tests__/suspense.test.tsx +++ b/packages/react-query/src/__tests__/suspense.test.tsx @@ -90,7 +90,7 @@ describe('useSuspenseQuery', () => { return (
- data: {state.data?.pages.join(',')} + data: {state.data.pages.join(',')}
) } @@ -264,6 +264,49 @@ describe('useSuspenseQuery', () => { consoleMock.mockRestore() }) + it('should throw to an ErrorBoundary if select throws an error', async () => { + const consoleMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) + const key = queryKey() + + function Page() { + useSuspenseQuery({ + queryKey: key, + queryFn: async () => { + await sleep(10) + return 'data' + }, + select: () => { + throw new Error('oh on') + }, + }) + + return
rendered
+ } + + const rendered = renderWithClient( + queryClient, + ( +
+
error boundary
+
+ )} + > + + + +
, + ) + + await waitFor(() => rendered.getByText('Loading...')) + + await waitFor(() => rendered.getByText('error boundary')) + + consoleMock.mockRestore() + }) + it('should retry fetch if the reset error boundary has been reset', async () => { const consoleMock = vi .spyOn(console, 'error') diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 7721812058..781b210112 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -52,13 +52,18 @@ export const getHasError = < result, errorResetBoundary, throwOnError, + suspense, query, }: { result: QueryObserverResult errorResetBoundary: QueryErrorResetBoundaryValue throwOnError: ThrowOnError + suspense: boolean | undefined query: Query | undefined }) => { + if (suspense && result.data === undefined) { + return true + } return ( result.isError && !errorResetBoundary.isReset() && diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index c70230a7b6..5036bef300 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -112,6 +112,7 @@ export function useBaseQuery< result, errorResetBoundary, throwOnError: defaultedOptions.throwOnError, + suspense: defaultedOptions.suspense, query: client .getQueryCache() .get< diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 1360426763..228e00fc79 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -332,6 +332,7 @@ export function useQueries< result, errorResetBoundary, throwOnError: query.throwOnError, + suspense: query.suspense, query: client.getQueryCache().get(query.queryHash), }) ) From 70c36148f7ffed136778abe6c9b3074dd2f88333 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 26 Jul 2024 11:54:51 +0200 Subject: [PATCH 2/3] refactor: exposes noop from query-core and use it in adapters --- packages/angular-query-experimental/src/inject-mutation.ts | 4 ++-- packages/angular-query-experimental/src/util/index.ts | 7 +++++-- packages/query-core/src/index.ts | 1 + packages/query-core/src/utils.ts | 4 +--- packages/react-query/src/useMutation.ts | 4 ++-- packages/react-query/src/utils.ts | 2 -- packages/solid-query/src/createMutation.ts | 4 ++-- packages/solid-query/src/utils.ts | 2 -- packages/svelte-query/src/createMutation.ts | 4 ++-- packages/svelte-query/src/utils.ts | 2 -- 10 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 4ec2c304b9..27d0b6b3fd 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -8,11 +8,11 @@ import { runInInjectionContext, signal, } from '@angular/core' -import { MutationObserver, notifyManager } from '@tanstack/query-core' +import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { assertInjector } from './util/assert-injector/assert-injector' import { signalProxy } from './signal-proxy' import { injectQueryClient } from './inject-query-client' -import { noop, shouldThrowError } from './util' +import { shouldThrowError } from './util' import { lazyInit } from './util/lazy-init/lazy-init' import type { diff --git a/packages/angular-query-experimental/src/util/index.ts b/packages/angular-query-experimental/src/util/index.ts index d07a9316fe..0ca9d8fe55 100644 --- a/packages/angular-query-experimental/src/util/index.ts +++ b/packages/angular-query-experimental/src/util/index.ts @@ -1,3 +1,8 @@ +/** + * + * @param throwError + * @param params + */ export function shouldThrowError) => boolean>( throwError: boolean | T | undefined, params: Parameters, @@ -9,5 +14,3 @@ export function shouldThrowError) => boolean>( return !!throwError } - -export function noop() {} diff --git a/packages/query-core/src/index.ts b/packages/query-core/src/index.ts index 0d6dd2f264..56c4f64c37 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -15,6 +15,7 @@ export { focusManager } from './focusManager' export { onlineManager } from './onlineManager' export { hashKey, + noop, replaceEqualDeep, isServer, matchQuery, diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index f84fdcd775..41f8289589 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -68,9 +68,7 @@ export type QueryTypeFilter = 'all' | 'active' | 'inactive' export const isServer = typeof window === 'undefined' || 'Deno' in globalThis -export function noop(): undefined { - return undefined -} +export function noop(): undefined {} export function functionalUpdate( updater: Updater, diff --git a/packages/react-query/src/useMutation.ts b/packages/react-query/src/useMutation.ts index 34edd450a2..f9e2de47a8 100644 --- a/packages/react-query/src/useMutation.ts +++ b/packages/react-query/src/useMutation.ts @@ -1,8 +1,8 @@ 'use client' import * as React from 'react' -import { MutationObserver, notifyManager } from '@tanstack/query-core' +import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' -import { noop, shouldThrowError } from './utils' +import { shouldThrowError } from './utils' import type { UseMutateFunction, UseMutationOptions, diff --git a/packages/react-query/src/utils.ts b/packages/react-query/src/utils.ts index d07a9316fe..b3a928ff6b 100644 --- a/packages/react-query/src/utils.ts +++ b/packages/react-query/src/utils.ts @@ -9,5 +9,3 @@ export function shouldThrowError) => boolean>( return !!throwError } - -export function noop() {} diff --git a/packages/solid-query/src/createMutation.ts b/packages/solid-query/src/createMutation.ts index 39c56ff2e3..4080fb8c41 100644 --- a/packages/solid-query/src/createMutation.ts +++ b/packages/solid-query/src/createMutation.ts @@ -1,8 +1,8 @@ -import { MutationObserver } from '@tanstack/query-core' +import { MutationObserver, noop } from '@tanstack/query-core' import { createComputed, createMemo, on, onCleanup } from 'solid-js' import { createStore } from 'solid-js/store' import { useQueryClient } from './QueryClientProvider' -import { noop, shouldThrowError } from './utils' +import { shouldThrowError } from './utils' import type { DefaultError } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' import type { diff --git a/packages/solid-query/src/utils.ts b/packages/solid-query/src/utils.ts index d07a9316fe..b3a928ff6b 100644 --- a/packages/solid-query/src/utils.ts +++ b/packages/solid-query/src/utils.ts @@ -9,5 +9,3 @@ export function shouldThrowError) => boolean>( return !!throwError } - -export function noop() {} diff --git a/packages/svelte-query/src/createMutation.ts b/packages/svelte-query/src/createMutation.ts index 97053fb0f4..d32d3df428 100644 --- a/packages/svelte-query/src/createMutation.ts +++ b/packages/svelte-query/src/createMutation.ts @@ -1,7 +1,7 @@ import { derived, get, readable } from 'svelte/store' -import { MutationObserver, notifyManager } from '@tanstack/query-core' +import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient' -import { isSvelteStore, noop } from './utils' +import { isSvelteStore } from './utils' import type { CreateMutateFunction, CreateMutationOptions, diff --git a/packages/svelte-query/src/utils.ts b/packages/svelte-query/src/utils.ts index 617144fae0..eb86225694 100644 --- a/packages/svelte-query/src/utils.ts +++ b/packages/svelte-query/src/utils.ts @@ -6,5 +6,3 @@ export function isSvelteStore( ): obj is Readable { return 'subscribe' in obj && typeof obj.subscribe === 'function' } - -export function noop() {} From acb1c0a51f181206f7c1ef888a5e810c844b7a70 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Fri, 26 Jul 2024 11:56:49 +0200 Subject: [PATCH 3/3] refactor: move shouldThrowOnError to errorBoundaryUtils.ts --- packages/react-query/src/errorBoundaryUtils.ts | 13 ++++++++++++- packages/react-query/src/useMutation.ts | 2 +- packages/react-query/src/utils.ts | 11 ----------- 3 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 packages/react-query/src/utils.ts diff --git a/packages/react-query/src/errorBoundaryUtils.ts b/packages/react-query/src/errorBoundaryUtils.ts index 781b210112..89a518bdc3 100644 --- a/packages/react-query/src/errorBoundaryUtils.ts +++ b/packages/react-query/src/errorBoundaryUtils.ts @@ -1,6 +1,5 @@ 'use client' import * as React from 'react' -import { shouldThrowError } from './utils' import type { DefaultedQueryObserverOptions, Query, @@ -42,6 +41,18 @@ export const useClearResetErrorBoundary = ( }, [errorResetBoundary]) } +export function shouldThrowError) => boolean>( + throwError: boolean | T | undefined, + params: Parameters, +): boolean { + // Allow throwError function to override throwing behavior on a per-error basis + if (typeof throwError === 'function') { + return throwError(...params) + } + + return !!throwError +} + export const getHasError = < TData, TError, diff --git a/packages/react-query/src/useMutation.ts b/packages/react-query/src/useMutation.ts index f9e2de47a8..584ee2cf54 100644 --- a/packages/react-query/src/useMutation.ts +++ b/packages/react-query/src/useMutation.ts @@ -2,7 +2,7 @@ import * as React from 'react' import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './QueryClientProvider' -import { shouldThrowError } from './utils' +import { shouldThrowError } from './errorBoundaryUtils' import type { UseMutateFunction, UseMutationOptions, diff --git a/packages/react-query/src/utils.ts b/packages/react-query/src/utils.ts deleted file mode 100644 index b3a928ff6b..0000000000 --- a/packages/react-query/src/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function shouldThrowError) => boolean>( - throwError: boolean | T | undefined, - params: Parameters, -): boolean { - // Allow throwError function to override throwing behavior on a per-error basis - if (typeof throwError === 'function') { - return throwError(...params) - } - - return !!throwError -}