Skip to content

Commit

Permalink
fix(react-query): ensureSuspenseTimers should ALWAYS set staleTime to…
Browse files Browse the repository at this point in the history
… 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]>
  • Loading branch information
3 people authored Jan 3, 2025
1 parent bc8b9e7 commit 0503282
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 60 deletions.
194 changes: 194 additions & 0 deletions packages/react-query/src/__tests__/suspense.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { act, render, waitFor } from '@testing-library/react'
import { Suspense } from 'react'
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest'
import { QueryClient, QueryClientProvider, useSuspenseQuery } from '..'
import { queryKey } from './utils'
import type { QueryKey } from '..'

function renderWithSuspense(client: QueryClient, ui: React.ReactNode) {
return render(
<QueryClientProvider client={client}>
<Suspense fallback="loading">{ui}</Suspense>
</QueryClientProvider>,
)
}

function createTestQuery(options: {
fetchCount: { count: number }
queryKey: QueryKey
staleTime?: number | (() => number)
}) {
return function TestComponent() {
const { data } = useSuspenseQuery({
queryKey: options.queryKey,
queryFn: () => {
options.fetchCount.count++
return 'data'
},
staleTime: options.staleTime,
})
return <div>data: {data}</div>
}
}

describe('Suspense Timer Tests', () => {
let queryClient: QueryClient
let fetchCount: { count: number }

beforeAll(() => {
vi.useFakeTimers({ shouldAdvanceTime: true })
})

afterAll(() => {
vi.useRealTimers()
})

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
fetchCount = { count: 0 }
})

it('should enforce minimum staleTime of 1000ms when using suspense with number', async () => {
const TestComponent = createTestQuery({
fetchCount,
queryKey: ['test'],
staleTime: 10,
})

const rendered = renderWithSuspense(queryClient, <TestComponent />)

await waitFor(() => rendered.getByText('data: data'))

rendered.rerender(
<QueryClientProvider client={queryClient}>
<Suspense fallback="loading">
<TestComponent />
</Suspense>
</QueryClientProvider>,
)

act(() => {
vi.advanceTimersByTime(100)
})

expect(fetchCount.count).toBe(1)
})

it('should enforce minimum staleTime of 1000ms when using suspense with function', async () => {
const TestComponent = createTestQuery({
fetchCount,
queryKey: ['test-func'],
staleTime: () => 10,
})

const rendered = renderWithSuspense(queryClient, <TestComponent />)

await waitFor(() => rendered.getByText('data: data'))

rendered.rerender(
<QueryClientProvider client={queryClient}>
<Suspense fallback="loading">
<TestComponent />
</Suspense>
</QueryClientProvider>,
)

act(() => {
vi.advanceTimersByTime(100)
})

expect(fetchCount.count).toBe(1)
})

it('should respect staleTime when value is greater than 1000ms', async () => {
const TestComponent = createTestQuery({
fetchCount,
queryKey: queryKey(),
staleTime: 2000,
})

const rendered = renderWithSuspense(queryClient, <TestComponent />)

await waitFor(() => rendered.getByText('data: data'))

rendered.rerender(
<QueryClientProvider client={queryClient}>
<Suspense fallback="loading">
<TestComponent />
</Suspense>
</QueryClientProvider>,
)

act(() => {
vi.advanceTimersByTime(1500)
})

expect(fetchCount.count).toBe(1)
})

it('should enforce minimum staleTime when undefined is provided', async () => {
const TestComponent = createTestQuery({
fetchCount,
queryKey: queryKey(),
staleTime: undefined,
})

const rendered = renderWithSuspense(queryClient, <TestComponent />)

await waitFor(() => rendered.getByText('data: data'))

rendered.rerender(
<QueryClientProvider client={queryClient}>
<Suspense fallback="loading">
<TestComponent />
</Suspense>
</QueryClientProvider>,
)

act(() => {
vi.advanceTimersByTime(500)
})

expect(fetchCount.count).toBe(1)
})

it('should respect staleTime when function returns value greater than 1000ms', async () => {
const TestComponent = createTestQuery({
fetchCount,
queryKey: queryKey(),
staleTime: () => 3000,
})

const rendered = renderWithSuspense(queryClient, <TestComponent />)

await waitFor(() => rendered.getByText('data: data'))

rendered.rerender(
<QueryClientProvider client={queryClient}>
<Suspense fallback="loading">
<TestComponent />
</Suspense>
</QueryClientProvider>,
)

act(() => {
vi.advanceTimersByTime(2000)
})

expect(fetchCount.count).toBe(1)
})
})
55 changes: 0 additions & 55 deletions packages/react-query/src/__tests__/useSuspenseQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,61 +327,6 @@ describe('useSuspenseQuery', () => {
consoleMock.mockRestore()
})

it('should refetch when re-mounting', async () => {
const key = queryKey()
let count = 0

function Component() {
const result = useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(100)
count++
return count
},
retry: false,
staleTime: 0,
})
return (
<div>
<span>data: {result.data}</span>
<span>fetching: {result.isFetching ? 'true' : 'false'}</span>
</div>
)
}

function Page() {
const [show, setShow] = React.useState(true)
return (
<div>
<button
onClick={() => {
setShow(!show)
}}
>
{show ? 'hide' : 'show'}
</button>
<React.Suspense fallback="Loading...">
{show && <Component />}
</React.Suspense>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)

await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('data: 1'))
await waitFor(() => rendered.getByText('fetching: false'))
await waitFor(() => rendered.getByText('hide'))
fireEvent.click(rendered.getByText('hide'))
await waitFor(() => rendered.getByText('show'))
fireEvent.click(rendered.getByText('show'))
await waitFor(() => rendered.getByText('fetching: true'))
await waitFor(() => rendered.getByText('data: 2'))
await waitFor(() => rendered.getByText('fetching: false'))
})

it('should set staleTime when having passed a function', async () => {
const key = queryKey()
let count = 0
Expand Down
14 changes: 9 additions & 5 deletions packages/react-query/src/suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ export const defaultThrowOnError = <
export const ensureSuspenseTimers = (
defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
) => {
const originalStaleTime = defaultedOptions.staleTime

if (defaultedOptions.suspense) {
// Always set stale time when using suspense to prevent
// fetching again when directly mounting after suspending
if (defaultedOptions.staleTime === undefined) {
defaultedOptions.staleTime = 1000
}
// Handle staleTime to ensure minimum 1000ms in Suspense mode
// This prevents unnecessary refetching when components remount after suspending
defaultedOptions.staleTime =
typeof originalStaleTime === 'function'
? (...args) => Math.max(originalStaleTime(...args), 1000)
: Math.max(originalStaleTime ?? 1000, 1000)

if (typeof defaultedOptions.gcTime === 'number') {
defaultedOptions.gcTime = Math.max(defaultedOptions.gcTime, 1000)
}
Expand Down

0 comments on commit 0503282

Please sign in to comment.