Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tasty-ears-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-query': patch
---

react(suspense): switch from throwing promises to use()
42 changes: 26 additions & 16 deletions packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
useSuspenseQueries,
useSuspenseQuery,
} from '..'
import { renderWithClient } from './utils'
import { renderWithClient, renderWithSuspense } from './utils'

describe('QueryErrorResetBoundary', () => {
let queryCache: QueryCache
Expand Down Expand Up @@ -84,7 +84,9 @@ describe('QueryErrorResetBoundary', () => {

succeed = true

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data')).toBeInTheDocument()

Expand Down Expand Up @@ -151,7 +153,9 @@ describe('QueryErrorResetBoundary', () => {

succeed = true

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('status: error')).toBeInTheDocument()

Expand Down Expand Up @@ -219,7 +223,9 @@ describe('QueryErrorResetBoundary', () => {

succeed = true

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data')).toBeInTheDocument()

Expand Down Expand Up @@ -593,7 +599,7 @@ describe('QueryErrorResetBoundary', () => {
return <div>{data}</div>
}

const rendered = renderWithClient(
const rendered = await renderWithSuspense(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
Expand All @@ -612,9 +618,7 @@ describe('QueryErrorResetBoundary', () => {
</div>
)}
>
<React.Suspense fallback={<div>loading</div>}>
<Page />
</React.Suspense>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
Expand All @@ -625,13 +629,17 @@ describe('QueryErrorResetBoundary', () => {
expect(rendered.getByText('error boundary')).toBeInTheDocument()
expect(rendered.getByText('retry')).toBeInTheDocument()

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
expect(rendered.getByText('error boundary')).toBeInTheDocument()
expect(rendered.getByText('retry')).toBeInTheDocument()

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
expect(rendered.getByText('data')).toBeInTheDocument()
Expand Down Expand Up @@ -722,7 +730,9 @@ describe('QueryErrorResetBoundary', () => {

succeed = true

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data')).toBeInTheDocument()

Expand Down Expand Up @@ -902,7 +912,7 @@ describe('QueryErrorResetBoundary', () => {
return <div>{data}</div>
}

const rendered = renderWithClient(
const rendered = await renderWithSuspense(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
Expand All @@ -921,9 +931,7 @@ describe('QueryErrorResetBoundary', () => {
</div>
)}
>
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
Expand All @@ -936,7 +944,9 @@ describe('QueryErrorResetBoundary', () => {

succeed = true

fireEvent.click(rendered.getByText('retry'))
await act(async () => {
fireEvent.click(rendered.getByText('retry'))
})
expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
expect(rendered.getByText('data')).toBeInTheDocument()
Expand Down
89 changes: 73 additions & 16 deletions packages/react-query/src/__tests__/suspense.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, render } from '@testing-library/react'
import { act } from '@testing-library/react'
import { Suspense } from 'react'
import { QueryObserver } from '@tanstack/query-core'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryClient, QueryClientProvider, useSuspenseQuery } from '..'
import { fallbackUse, getSuspensePromise } from '../suspense'
import { renderWithSuspense } from './utils'
import type { StaleTime } from '@tanstack/query-core'
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
Expand Down Expand Up @@ -54,14 +49,76 @@ describe('Suspense Timer Tests', () => {
vi.useRealTimers()
})

it('should reuse the suspense promise while a query is pending', async () => {
const key = queryKey()
const options = queryClient.defaultQueryOptions({
queryKey: key,
queryFn: () => sleep(10).then(() => 'data'),
suspense: true,
})
const observer = new QueryObserver(queryClient, options)
const errorResetBoundary = {
clearReset: vi.fn(),
isReset: () => false,
reset: vi.fn(),
}

const firstPromise = getSuspensePromise(
options,
observer,
errorResetBoundary,
)
const secondPromise = getSuspensePromise(
options,
observer,
errorResetBoundary,
)

expect(secondPromise).toBe(firstPromise)

await vi.advanceTimersByTimeAsync(10)
await firstPromise
})

it('should support pending, fulfilled, and rejected promise states in the React 18 fallback', async () => {
let resolvePending!: (value: string) => void
const pending = new Promise<string>((resolve) => {
resolvePending = resolve
})

let thrown: unknown
try {
fallbackUse(pending)
} catch (error) {
thrown = error
}
expect(thrown).toBe(pending)

resolvePending('data')
await pending
expect(fallbackUse(pending)).toBe('data')

const error = new Error('error')
const rejected = Promise.reject(error)
try {
fallbackUse(rejected)
} catch (rejectedPromise) {
thrown = rejectedPromise
}
expect(thrown).toBe(rejected)

await rejected.catch(() => undefined)
expect(() => fallbackUse(rejected)).toThrow(error)
})

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

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand All @@ -87,7 +144,7 @@ describe('Suspense Timer Tests', () => {
staleTime: () => 10,
})

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand All @@ -113,7 +170,7 @@ describe('Suspense Timer Tests', () => {
staleTime: 2000,
})

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand All @@ -139,7 +196,7 @@ describe('Suspense Timer Tests', () => {
staleTime: undefined,
})

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand All @@ -165,7 +222,7 @@ describe('Suspense Timer Tests', () => {
staleTime: 'static',
})

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand All @@ -191,7 +248,7 @@ describe('Suspense Timer Tests', () => {
staleTime: () => 'static',
})

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand All @@ -217,7 +274,7 @@ describe('Suspense Timer Tests', () => {
staleTime: () => 3000,
})

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

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
usePrefetchInfiniteQuery,
useSuspenseInfiniteQuery,
} from '..'
import { renderWithClient } from './utils'
import { renderWithSuspense } from './utils'

describe('usePrefetchInfiniteQuery', () => {
let queryCache: QueryCache
Expand Down Expand Up @@ -68,15 +68,19 @@ describe('usePrefetchInfiniteQuery', () => {
)
}

const rendered = renderWithClient(queryClient, <App />)
const rendered = await renderWithSuspense(queryClient, <App />)

await act(() => vi.advanceTimersByTimeAsync(30))
rendered.getByText('data: Do you fetch on render?')
fireEvent.click(rendered.getByText('Next Page'))
await act(async () => {
fireEvent.click(rendered.getByText('Next Page'))
})
expect(
rendered.getByText('data: Or do you render as you fetch?'),
).toBeInTheDocument()
fireEvent.click(rendered.getByText('Next Page'))
await act(async () => {
fireEvent.click(rendered.getByText('Next Page'))
})
expect(
rendered.getByText('data: Either way, Tanstack Query helps you!'),
).toBeInTheDocument()
Expand Down Expand Up @@ -131,12 +135,16 @@ describe('usePrefetchInfiniteQuery', () => {
)
}

const rendered = renderWithClient(queryClient, <App />)
const rendered = await renderWithSuspense(queryClient, <App />)

expect(rendered.getByText('data: Prefetch rocks!')).toBeInTheDocument()
fireEvent.click(rendered.getByText('Next Page'))
await act(async () => {
fireEvent.click(rendered.getByText('Next Page'))
})
expect(rendered.getByText('data: No waterfalls, boy!')).toBeInTheDocument()
fireEvent.click(rendered.getByText('Next Page'))
await act(async () => {
fireEvent.click(rendered.getByText('Next Page'))
})
expect(rendered.getByText('data: Tanstack Query #ftw')).toBeInTheDocument()
expect(queryOpts.queryFn).not.toHaveBeenCalled()
expect(Fallback).not.toHaveBeenCalled()
Expand Down Expand Up @@ -187,14 +195,18 @@ describe('usePrefetchInfiniteQuery', () => {
)
}

const rendered = renderWithClient(queryClient, <App />)
const rendered = await renderWithSuspense(queryClient, <App />)

await act(() => vi.advanceTimersByTimeAsync(10))
rendered.getByText('data: Infinite Page 1')
fireEvent.click(rendered.getByText('Next Page'))
await act(async () => {
fireEvent.click(rendered.getByText('Next Page'))
})
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data: Infinite Page 2')).toBeInTheDocument()
fireEvent.click(rendered.getByText('Next Page'))
await act(async () => {
fireEvent.click(rendered.getByText('Next Page'))
})
await vi.advanceTimersByTimeAsync(11)
expect(rendered.getByText('data: Infinite Page 3')).toBeInTheDocument()
expect(queryOpts.queryFn).toHaveBeenCalledTimes(3)
Expand Down
Loading
Loading