diff --git a/packages/eslint-plugin-query/src/__tests__/no-void-query-fn.test.ts b/packages/eslint-plugin-query/src/__tests__/no-void-query-fn.test.ts index c136a2582c0..e26c21de8f6 100644 --- a/packages/eslint-plugin-query/src/__tests__/no-void-query-fn.test.ts +++ b/packages/eslint-plugin-query/src/__tests__/no-void-query-fn.test.ts @@ -162,6 +162,59 @@ ruleTester.run('no-void-query-fn', rule, { } `, }, + { + name: 'useInfiniteQuery queryFn returns a value', + code: normalizeIndent` + import { useInfiniteQuery } from '@tanstack/react-query' + + function Component() { + const query = useInfiniteQuery({ + queryKey: ['test'], + queryFn: ({ pageParam }) => ({ data: 'test', page: pageParam }), + initialPageParam: 0, + getNextPageParam: (lastPage) => undefined, + }) + return null + } + `, + }, + { + name: 'useSuspenseQuery queryFn returns a value', + code: normalizeIndent` + import { useSuspenseQuery } from '@tanstack/react-query' + + function Component() { + const query = useSuspenseQuery({ + queryKey: ['test'], + queryFn: () => ({ data: 'test' }), + }) + return null + } + `, + }, + { + name: 'queryOptions queryFn returns a value', + code: normalizeIndent` + import { queryOptions } from '@tanstack/react-query' + + const options = queryOptions({ + queryKey: ['test'], + queryFn: () => ({ data: 'test' }), + }) + `, + }, + { + name: 'fetchQuery queryFn returns a value', + code: normalizeIndent` + import { QueryClient } from '@tanstack/react-query' + + const queryClient = new QueryClient() + queryClient.fetchQuery({ + queryKey: ['test'], + queryFn: () => fetch('/api/test').then((r) => r.json()), + }) + `, + }, ], invalid: [ { @@ -321,5 +374,70 @@ ruleTester.run('no-void-query-fn', rule, { `, errors: [{ messageId: 'noVoidReturn' }], }, + { + name: 'useInfiniteQuery queryFn returns void', + code: normalizeIndent` + import { useInfiniteQuery } from '@tanstack/react-query' + + function Component() { + const query = useInfiniteQuery({ + queryKey: ['test'], + queryFn: async ({ pageParam }) => { + await fetch('/api/test?page=' + pageParam) + }, + initialPageParam: 0, + getNextPageParam: (lastPage) => undefined, + }) + return null + } + `, + errors: [{ messageId: 'noVoidReturn' }], + }, + { + name: 'useSuspenseQuery queryFn returns void', + code: normalizeIndent` + import { useSuspenseQuery } from '@tanstack/react-query' + + function Component() { + const query = useSuspenseQuery({ + queryKey: ['test'], + queryFn: () => { + console.log('fetching') + }, + }) + return null + } + `, + errors: [{ messageId: 'noVoidReturn' }], + }, + { + name: 'queryOptions queryFn returns void', + code: normalizeIndent` + import { queryOptions } from '@tanstack/react-query' + + const options = queryOptions({ + queryKey: ['test'], + queryFn: async () => { + await fetch('/api/test') + }, + }) + `, + errors: [{ messageId: 'noVoidReturn' }], + }, + { + name: 'fetchQuery queryFn returns void', + code: normalizeIndent` + import { QueryClient } from '@tanstack/react-query' + + const queryClient = new QueryClient() + queryClient.fetchQuery({ + queryKey: ['test'], + queryFn: async () => { + await fetch('/api/test') + }, + }) + `, + errors: [{ messageId: 'noVoidReturn' }], + }, ], }) diff --git a/packages/preact-query/src/__tests__/useSuspenseQueries.test.tsx b/packages/preact-query/src/__tests__/useSuspenseQueries.test.tsx index 5445fa95b0d..8fa4072c69c 100644 --- a/packages/preact-query/src/__tests__/useSuspenseQueries.test.tsx +++ b/packages/preact-query/src/__tests__/useSuspenseQueries.test.tsx @@ -1078,6 +1078,74 @@ describe('useSuspenseQueries 2', () => { expect(queryFn1).toHaveBeenCalledTimes(0) }) + it('should not suspend and only refetch the stale query when one query has stale and the other has fresh cached data', async () => { + const key1 = queryKey() + const key2 = queryKey() + + queryClient.setQueryData(key1, 'cached1') + queryClient.setQueryData(key2, 'cached2') + + // Advance past staleTime (min 1000ms in suspense) so key1 becomes stale before mount + vi.advanceTimersByTime(1000) + + // Make key2 fresh again by resetting its data + queryClient.setQueryData(key2, 'cached2') + + const queryFn2 = vi.fn(() => sleep(20).then(() => 'data2')) + + function Page() { + const [result1, result2] = useSuspenseQueries({ + queries: [ + { + queryKey: key1, + queryFn: () => sleep(10).then(() => 'data1'), + }, + { + queryKey: key2, + queryFn: queryFn2, + }, + ], + }) + + return ( +
+
data1: {result1.data}
+
data2: {result2.data}
+
+ ) + } + + const rendered = renderWithClient( + queryClient, + loading}> + + , + ) + + // No suspend, cached data shown immediately + expect(rendered.getByText('data1: cached1')).toBeInTheDocument() + expect(rendered.getByText('data2: cached2')).toBeInTheDocument() + + // key2 is fresh, no refetch + expect(queryFn2).toHaveBeenCalledTimes(0) + + // key1 background refetch completes + await vi.advanceTimersByTimeAsync(11) + + expect(rendered.getByText('data1: data1')).toBeInTheDocument() + expect(rendered.getByText('data2: cached2')).toBeInTheDocument() + + // key2 is still fresh, no refetch triggered + expect(queryFn2).toHaveBeenCalledTimes(0) + + // after key1 refetch completes, key2 is still fresh with no refetch triggered + await vi.advanceTimersByTimeAsync(10) + + expect(rendered.getByText('data1: data1')).toBeInTheDocument() + expect(rendered.getByText('data2: cached2')).toBeInTheDocument() + expect(queryFn2).toHaveBeenCalledTimes(0) + }) + it('should not suspend but refetch when all queries have stale cached data', async () => { const key1 = queryKey() const key2 = queryKey() diff --git a/packages/react-query/src/__tests__/useSuspenseQueries.test.tsx b/packages/react-query/src/__tests__/useSuspenseQueries.test.tsx index 9f114cd94f4..4ebd32ea90b 100644 --- a/packages/react-query/src/__tests__/useSuspenseQueries.test.tsx +++ b/packages/react-query/src/__tests__/useSuspenseQueries.test.tsx @@ -1040,7 +1040,7 @@ describe('useSuspenseQueries 2', () => { expect(queryFn1).toHaveBeenCalledTimes(0) // key2 background refetch completes - await act(() => vi.advanceTimersByTimeAsync(11)) + await vi.advanceTimersByTimeAsync(11) expect(rendered.getByText('data1: cached1')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() @@ -1049,13 +1049,81 @@ describe('useSuspenseQueries 2', () => { expect(queryFn1).toHaveBeenCalledTimes(0) // after key2 refetch completes, key1 is still fresh with no refetch triggered - await act(() => vi.advanceTimersByTimeAsync(10)) + await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('data1: cached1')).toBeInTheDocument() expect(rendered.getByText('data2: data2')).toBeInTheDocument() expect(queryFn1).toHaveBeenCalledTimes(0) }) + it('should not suspend and only refetch the stale query when one query has stale and the other has fresh cached data', async () => { + const key1 = queryKey() + const key2 = queryKey() + + queryClient.setQueryData(key1, 'cached1') + queryClient.setQueryData(key2, 'cached2') + + // Advance past staleTime (min 1000ms in suspense) so key1 becomes stale before mount + vi.advanceTimersByTime(1000) + + // Make key2 fresh again by resetting its data + queryClient.setQueryData(key2, 'cached2') + + const queryFn2 = vi.fn(() => sleep(20).then(() => 'data2')) + + function Page() { + const [result1, result2] = useSuspenseQueries({ + queries: [ + { + queryKey: key1, + queryFn: () => sleep(10).then(() => 'data1'), + }, + { + queryKey: key2, + queryFn: queryFn2, + }, + ], + }) + + return ( +
+
data1: {result1.data}
+
data2: {result2.data}
+
+ ) + } + + const rendered = renderWithClient( + queryClient, + loading}> + + , + ) + + // No suspend, cached data shown immediately + expect(rendered.getByText('data1: cached1')).toBeInTheDocument() + expect(rendered.getByText('data2: cached2')).toBeInTheDocument() + + // key2 is fresh, no refetch + expect(queryFn2).toHaveBeenCalledTimes(0) + + // key1 background refetch completes + await vi.advanceTimersByTimeAsync(11) + + expect(rendered.getByText('data1: data1')).toBeInTheDocument() + expect(rendered.getByText('data2: cached2')).toBeInTheDocument() + + // key2 is still fresh, no refetch triggered + expect(queryFn2).toHaveBeenCalledTimes(0) + + // after key1 refetch completes, key2 is still fresh with no refetch triggered + await vi.advanceTimersByTimeAsync(10) + + expect(rendered.getByText('data1: data1')).toBeInTheDocument() + expect(rendered.getByText('data2: cached2')).toBeInTheDocument() + expect(queryFn2).toHaveBeenCalledTimes(0) + }) + it('should not suspend but refetch when all queries have stale cached data', async () => { const key1 = queryKey() const key2 = queryKey()