diff --git a/packages/react-query/src/__tests__/suspense.test.tsx b/packages/react-query/src/__tests__/suspense.test.tsx
index 52ab4d6c4e..cc1d029697 100644
--- a/packages/react-query/src/__tests__/suspense.test.tsx
+++ b/packages/react-query/src/__tests__/suspense.test.tsx
@@ -1238,3 +1238,233 @@ describe('useQueries with suspense', () => {
expect(results).toEqual(['1', '2', 'loading'])
})
})
+
+describe('cacheTime minimum enforcement with suspense', () => {
+ const queryClient = createQueryClient()
+
+ it('should not cause infinite re-renders with synchronous query function and cacheTime: 0', async () => {
+ const key = queryKey()
+ let renderCount = 0
+ let queryFnCallCount = 0
+ const maxChecks = 20
+
+ function Page() {
+ renderCount++
+
+ if (renderCount > maxChecks) {
+ throw new Error(`Infinite loop detected! Renders: ${renderCount}`)
+ }
+
+ const result = useQuery(
+ key,
+ () => {
+ queryFnCallCount++
+ return 42
+ },
+ {
+ cacheTime: 0,
+ suspense: true,
+ },
+ )
+
+ return
data: {result.data}
+ }
+
+ const rendered = renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => rendered.getByText('data: 42'))
+
+ expect(renderCount).toBeLessThan(5)
+ expect(queryFnCallCount).toBe(1)
+ expect(rendered.queryByText('data: 42')).not.toBeNull()
+ expect(rendered.queryByText('loading')).toBeNull()
+ })
+
+ describe('boundary value tests', () => {
+ test.each([
+ [0, 1000],
+ [1, 1000],
+ [999, 1000],
+ [1000, 1000],
+ [2000, 2000],
+ ])(
+ 'cacheTime %i should be adjusted to %i with suspense',
+ async (input, expected) => {
+ const key = queryKey()
+
+ function Page() {
+ const result = useQuery(key, () => 42, {
+ suspense: true,
+ cacheTime: input,
+ })
+ return data: {result.data}
+ }
+
+ const rendered = renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => rendered.getByText('data: 42'))
+
+ const query = queryClient.getQueryCache().find(key)
+ const options = query?.options
+ expect(options?.cacheTime).toBe(expected)
+ },
+ )
+ })
+
+ it('should preserve user cacheTime when >= 1000ms', async () => {
+ const key = queryKey()
+ const userCacheTime = 5000
+
+ function Page() {
+ useQuery(key, () => 'test', {
+ suspense: true,
+ cacheTime: userCacheTime,
+ })
+ return rendered
+ }
+
+ renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => {
+ const query = queryClient.getQueryCache().find(key)
+ const options = query?.options
+ expect(options?.cacheTime).toBe(userCacheTime)
+ })
+ })
+
+ it('should handle async queries with adjusted cacheTime', async () => {
+ const key = queryKey()
+ let renderCount = 0
+
+ function Page() {
+ renderCount++
+ const result = useQuery(
+ key,
+ async () => {
+ await sleep(10)
+ return 'async-result'
+ },
+ {
+ suspense: true,
+ cacheTime: 0,
+ },
+ )
+ return data: {result.data}
+ }
+
+ const rendered = renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => rendered.getByText('data: async-result'))
+ expect(renderCount).toBeLessThan(5)
+ })
+
+ describe('staleTime and cacheTime relationship', () => {
+ it('should handle when both need adjustment', async () => {
+ const key = queryKey()
+
+ function Page() {
+ useQuery(key, () => 42, {
+ suspense: true,
+ cacheTime: 0,
+ staleTime: undefined,
+ })
+ return rendered
+ }
+
+ renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => {
+ const query = queryClient.getQueryCache().find(key)
+ const options = query?.options as any
+ expect(options?.cacheTime).toBe(1000)
+ expect(options?.staleTime).toBe(1000)
+ })
+ })
+
+ it('should maintain staleTime < cacheTime invariant', async () => {
+ const key = queryKey()
+
+ function Page() {
+ useQuery(key, () => 42, {
+ suspense: true,
+ cacheTime: 500,
+ staleTime: 2000,
+ })
+ return rendered
+ }
+
+ renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => {
+ const query = queryClient.getQueryCache().find(key)
+ const options = query?.options as any
+ expect(options?.cacheTime).toBe(1000)
+ expect(options?.staleTime).toBe(2000)
+ })
+ })
+ })
+
+ it('should fix synchronous query with cacheTime 0 infinite loop', async () => {
+ const key = queryKey()
+ let renderCount = 0
+ let queryFnCallCount = 0
+
+ function Page() {
+ renderCount++
+ const result = useQuery(
+ key,
+ () => {
+ queryFnCallCount++
+ return 42
+ },
+ {
+ suspense: true,
+ cacheTime: 0,
+ },
+ )
+ return data: {result.data}
+ }
+
+ const rendered = renderWithClient(
+ queryClient,
+
+
+ ,
+ )
+
+ await waitFor(() => rendered.getByText('data: 42'))
+
+ expect(renderCount).toBeLessThan(5)
+ expect(queryFnCallCount).toBe(1)
+ })
+})
diff --git a/packages/react-query/src/suspense.ts b/packages/react-query/src/suspense.ts
index 682409e75d..56afdae1f1 100644
--- a/packages/react-query/src/suspense.ts
+++ b/packages/react-query/src/suspense.ts
@@ -4,6 +4,13 @@ import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
import type { QueryObserverResult } from '@tanstack/query-core'
import type { QueryKey } from '@tanstack/query-core'
+/**
+ * Ensures minimum staleTime and cacheTime values when suspense is enabled.
+ * Despite the name, this function guards both staleTime and cacheTime to prevent
+ * infinite re-render loops with synchronous queries.
+ *
+ * @deprecated in v5 - replaced by ensureSuspenseTimers
+ */
export const ensureStaleTime = (
defaultedOptions: DefaultedQueryObserverOptions,
) => {
@@ -13,6 +20,10 @@ export const ensureStaleTime = (
if (typeof defaultedOptions.staleTime !== 'number') {
defaultedOptions.staleTime = 1000
}
+
+ if (typeof defaultedOptions.cacheTime === 'number') {
+ defaultedOptions.cacheTime = Math.max(defaultedOptions.cacheTime, 1000)
+ }
}
}