Skip to content

Commit 0503282

Browse files
minseong0324autofix-ci[bot]TkDodo
authored
fix(react-query): ensureSuspenseTimers should ALWAYS set staleTime to 1000 when it is below 1000. (#8398)
* fix(react-query): ensureSuspenseTimers should ALWAYS set staleTime to 1000 when it is below 1000. * ci: apply automated fixes * fix(react-query): fix ensureSuspenseTimers logic * test(react-query): add test code * test(react-query): delete unnecessary test code * test(react-query): Refectoring test code to reduce test time --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent bc8b9e7 commit 0503282

File tree

3 files changed

+203
-60
lines changed

3 files changed

+203
-60
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { act, render, waitFor } from '@testing-library/react'
2+
import { Suspense } from 'react'
3+
import {
4+
afterAll,
5+
beforeAll,
6+
beforeEach,
7+
describe,
8+
expect,
9+
it,
10+
vi,
11+
} from 'vitest'
12+
import { QueryClient, QueryClientProvider, useSuspenseQuery } from '..'
13+
import { queryKey } from './utils'
14+
import type { QueryKey } from '..'
15+
16+
function renderWithSuspense(client: QueryClient, ui: React.ReactNode) {
17+
return render(
18+
<QueryClientProvider client={client}>
19+
<Suspense fallback="loading">{ui}</Suspense>
20+
</QueryClientProvider>,
21+
)
22+
}
23+
24+
function createTestQuery(options: {
25+
fetchCount: { count: number }
26+
queryKey: QueryKey
27+
staleTime?: number | (() => number)
28+
}) {
29+
return function TestComponent() {
30+
const { data } = useSuspenseQuery({
31+
queryKey: options.queryKey,
32+
queryFn: () => {
33+
options.fetchCount.count++
34+
return 'data'
35+
},
36+
staleTime: options.staleTime,
37+
})
38+
return <div>data: {data}</div>
39+
}
40+
}
41+
42+
describe('Suspense Timer Tests', () => {
43+
let queryClient: QueryClient
44+
let fetchCount: { count: number }
45+
46+
beforeAll(() => {
47+
vi.useFakeTimers({ shouldAdvanceTime: true })
48+
})
49+
50+
afterAll(() => {
51+
vi.useRealTimers()
52+
})
53+
54+
beforeEach(() => {
55+
queryClient = new QueryClient({
56+
defaultOptions: {
57+
queries: {
58+
retry: false,
59+
},
60+
},
61+
})
62+
fetchCount = { count: 0 }
63+
})
64+
65+
it('should enforce minimum staleTime of 1000ms when using suspense with number', async () => {
66+
const TestComponent = createTestQuery({
67+
fetchCount,
68+
queryKey: ['test'],
69+
staleTime: 10,
70+
})
71+
72+
const rendered = renderWithSuspense(queryClient, <TestComponent />)
73+
74+
await waitFor(() => rendered.getByText('data: data'))
75+
76+
rendered.rerender(
77+
<QueryClientProvider client={queryClient}>
78+
<Suspense fallback="loading">
79+
<TestComponent />
80+
</Suspense>
81+
</QueryClientProvider>,
82+
)
83+
84+
act(() => {
85+
vi.advanceTimersByTime(100)
86+
})
87+
88+
expect(fetchCount.count).toBe(1)
89+
})
90+
91+
it('should enforce minimum staleTime of 1000ms when using suspense with function', async () => {
92+
const TestComponent = createTestQuery({
93+
fetchCount,
94+
queryKey: ['test-func'],
95+
staleTime: () => 10,
96+
})
97+
98+
const rendered = renderWithSuspense(queryClient, <TestComponent />)
99+
100+
await waitFor(() => rendered.getByText('data: data'))
101+
102+
rendered.rerender(
103+
<QueryClientProvider client={queryClient}>
104+
<Suspense fallback="loading">
105+
<TestComponent />
106+
</Suspense>
107+
</QueryClientProvider>,
108+
)
109+
110+
act(() => {
111+
vi.advanceTimersByTime(100)
112+
})
113+
114+
expect(fetchCount.count).toBe(1)
115+
})
116+
117+
it('should respect staleTime when value is greater than 1000ms', async () => {
118+
const TestComponent = createTestQuery({
119+
fetchCount,
120+
queryKey: queryKey(),
121+
staleTime: 2000,
122+
})
123+
124+
const rendered = renderWithSuspense(queryClient, <TestComponent />)
125+
126+
await waitFor(() => rendered.getByText('data: data'))
127+
128+
rendered.rerender(
129+
<QueryClientProvider client={queryClient}>
130+
<Suspense fallback="loading">
131+
<TestComponent />
132+
</Suspense>
133+
</QueryClientProvider>,
134+
)
135+
136+
act(() => {
137+
vi.advanceTimersByTime(1500)
138+
})
139+
140+
expect(fetchCount.count).toBe(1)
141+
})
142+
143+
it('should enforce minimum staleTime when undefined is provided', async () => {
144+
const TestComponent = createTestQuery({
145+
fetchCount,
146+
queryKey: queryKey(),
147+
staleTime: undefined,
148+
})
149+
150+
const rendered = renderWithSuspense(queryClient, <TestComponent />)
151+
152+
await waitFor(() => rendered.getByText('data: data'))
153+
154+
rendered.rerender(
155+
<QueryClientProvider client={queryClient}>
156+
<Suspense fallback="loading">
157+
<TestComponent />
158+
</Suspense>
159+
</QueryClientProvider>,
160+
)
161+
162+
act(() => {
163+
vi.advanceTimersByTime(500)
164+
})
165+
166+
expect(fetchCount.count).toBe(1)
167+
})
168+
169+
it('should respect staleTime when function returns value greater than 1000ms', async () => {
170+
const TestComponent = createTestQuery({
171+
fetchCount,
172+
queryKey: queryKey(),
173+
staleTime: () => 3000,
174+
})
175+
176+
const rendered = renderWithSuspense(queryClient, <TestComponent />)
177+
178+
await waitFor(() => rendered.getByText('data: data'))
179+
180+
rendered.rerender(
181+
<QueryClientProvider client={queryClient}>
182+
<Suspense fallback="loading">
183+
<TestComponent />
184+
</Suspense>
185+
</QueryClientProvider>,
186+
)
187+
188+
act(() => {
189+
vi.advanceTimersByTime(2000)
190+
})
191+
192+
expect(fetchCount.count).toBe(1)
193+
})
194+
})

packages/react-query/src/__tests__/useSuspenseQuery.test.tsx

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -327,61 +327,6 @@ describe('useSuspenseQuery', () => {
327327
consoleMock.mockRestore()
328328
})
329329

330-
it('should refetch when re-mounting', async () => {
331-
const key = queryKey()
332-
let count = 0
333-
334-
function Component() {
335-
const result = useSuspenseQuery({
336-
queryKey: key,
337-
queryFn: async () => {
338-
await sleep(100)
339-
count++
340-
return count
341-
},
342-
retry: false,
343-
staleTime: 0,
344-
})
345-
return (
346-
<div>
347-
<span>data: {result.data}</span>
348-
<span>fetching: {result.isFetching ? 'true' : 'false'}</span>
349-
</div>
350-
)
351-
}
352-
353-
function Page() {
354-
const [show, setShow] = React.useState(true)
355-
return (
356-
<div>
357-
<button
358-
onClick={() => {
359-
setShow(!show)
360-
}}
361-
>
362-
{show ? 'hide' : 'show'}
363-
</button>
364-
<React.Suspense fallback="Loading...">
365-
{show && <Component />}
366-
</React.Suspense>
367-
</div>
368-
)
369-
}
370-
371-
const rendered = renderWithClient(queryClient, <Page />)
372-
373-
await waitFor(() => rendered.getByText('Loading...'))
374-
await waitFor(() => rendered.getByText('data: 1'))
375-
await waitFor(() => rendered.getByText('fetching: false'))
376-
await waitFor(() => rendered.getByText('hide'))
377-
fireEvent.click(rendered.getByText('hide'))
378-
await waitFor(() => rendered.getByText('show'))
379-
fireEvent.click(rendered.getByText('show'))
380-
await waitFor(() => rendered.getByText('fetching: true'))
381-
await waitFor(() => rendered.getByText('data: 2'))
382-
await waitFor(() => rendered.getByText('fetching: false'))
383-
})
384-
385330
it('should set staleTime when having passed a function', async () => {
386331
const key = queryKey()
387332
let count = 0

packages/react-query/src/suspense.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ export const defaultThrowOnError = <
2121
export const ensureSuspenseTimers = (
2222
defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
2323
) => {
24+
const originalStaleTime = defaultedOptions.staleTime
25+
2426
if (defaultedOptions.suspense) {
25-
// Always set stale time when using suspense to prevent
26-
// fetching again when directly mounting after suspending
27-
if (defaultedOptions.staleTime === undefined) {
28-
defaultedOptions.staleTime = 1000
29-
}
27+
// Handle staleTime to ensure minimum 1000ms in Suspense mode
28+
// This prevents unnecessary refetching when components remount after suspending
29+
defaultedOptions.staleTime =
30+
typeof originalStaleTime === 'function'
31+
? (...args) => Math.max(originalStaleTime(...args), 1000)
32+
: Math.max(originalStaleTime ?? 1000, 1000)
33+
3034
if (typeof defaultedOptions.gcTime === 'number') {
3135
defaultedOptions.gcTime = Math.max(defaultedOptions.gcTime, 1000)
3236
}

0 commit comments

Comments
 (0)