Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions packages/query-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
dehydrate,
hydrate,
} from './hydration'
export { pendingThenable } from './thenable'
export { InfiniteQueryObserver } from './infiniteQueryObserver'
export { MutationCache } from './mutationCache'
export type { MutationCacheNotifyEvent } from './mutationCache'
Expand Down
168 changes: 14 additions & 154 deletions packages/react-query/src/__tests__/QueryResetErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import {
QueryErrorResetBoundary,
useQueries,
useQuery,
useSuspenseQueries,
useSuspenseQuery,
} from '..'
import { renderWithClient } from './utils'
import { renderWithClient, renderWithClientAndSuspense } from './utils'

describe('QueryErrorResetBoundary', () => {
let queryCache: QueryCache
Expand Down Expand Up @@ -53,7 +51,7 @@ describe('QueryErrorResetBoundary', () => {
return <div>{data}</div>
}

const rendered = renderWithClient(
const rendered = await renderWithClientAndSuspense(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
Expand Down Expand Up @@ -84,7 +82,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 @@ -120,7 +120,7 @@ describe('QueryErrorResetBoundary', () => {
)
}

const rendered = renderWithClient(
const rendered = await renderWithClientAndSuspense(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
Expand Down Expand Up @@ -151,7 +151,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 @@ -188,7 +190,7 @@ describe('QueryErrorResetBoundary', () => {
return <div>{data}</div>
}

const rendered = renderWithClient(
const rendered = await renderWithClientAndSuspense(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
Expand Down Expand Up @@ -219,7 +221,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 @@ -254,7 +258,7 @@ describe('QueryErrorResetBoundary', () => {
)
}

const rendered = renderWithClient(
const rendered = await renderWithClientAndSuspense(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
Expand Down Expand Up @@ -567,81 +571,6 @@ describe('QueryErrorResetBoundary', () => {
consoleMock.mockRestore()
})

it('should never render the component while the query is in error state', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)

const key = queryKey()
let fetchCount = 0
let renders = 0

function Page() {
const { data } = useSuspenseQuery({
queryKey: key,
queryFn: () =>
sleep(10).then(() => {
fetchCount++
if (fetchCount > 2) return 'data'
throw new Error('Error')
}),
retry: false,
})

renders++

return <div>{data}</div>
}

const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<React.Suspense fallback={<div>loading</div>}>
<Page />
</React.Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)

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'))
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'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
expect(rendered.getByText('data')).toBeInTheDocument()

expect(fetchCount).toBe(3)
expect(renders).toBe(1)

consoleMock.mockRestore()
})

it('should render children', () => {
const consoleMock = vi
.spyOn(console, 'error')
Expand Down Expand Up @@ -874,74 +803,5 @@ describe('QueryErrorResetBoundary', () => {

consoleMock.mockRestore()
})

it('with suspense should retry fetch if the reset error boundary has been reset', async () => {
const key = queryKey()
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)

let succeed = false

function Page() {
const [{ data }] = useSuspenseQueries({
queries: [
{
queryKey: key,
queryFn: () =>
sleep(10).then(() => {
if (!succeed) throw new Error('Error')
return 'data'
}),
retry: false,
retryOnMount: true,
},
],
})

return <div>{data}</div>
}

const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)

expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
expect(rendered.getByText('error boundary')).toBeInTheDocument()
expect(rendered.getByText('retry')).toBeInTheDocument()

succeed = true

fireEvent.click(rendered.getByText('retry'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(10))
expect(rendered.getByText('data')).toBeInTheDocument()

consoleMock.mockRestore()
})
})
})
66 changes: 66 additions & 0 deletions packages/react-query/src/__tests__/ssr-hydration.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import * as React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { act } from 'react'
import * as ReactDOMServer from 'react-dom/server'
Expand All @@ -9,6 +10,7 @@ import {
dehydrate,
hydrate,
useQuery,
useSuspenseQuery,
} from '..'
import { setIsServer } from './utils'

Expand Down Expand Up @@ -266,4 +268,68 @@ describe('Server side rendering with de/rehydration', () => {
queryClient.clear()
consoleMock.mockRestore()
})

it('should not expose undefined data from useSuspenseQuery during hydration with prefetched data', async () => {
const consoleMock = vi.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)

function ProfilesComponent() {
const profiles = useSuspenseQuery({
queryKey: ['profiles'],
queryFn: () => fetchData([{ profileId: 1, isDefault: true }]),
}).data

const activeProfile = profiles.find((profile) => profile.isDefault)

return <>{activeProfile?.profileId}</>
}

setIsServer(true)

const prefetchClient = new QueryClient()
await prefetchClient.prefetchQuery({
queryKey: ['profiles'],
queryFn: () => fetchData([{ profileId: 1, isDefault: true }]),
})

const dehydratedStateServer = dehydrate(prefetchClient)
const renderClient = new QueryClient()
hydrate(renderClient, dehydratedStateServer)

const markup = ReactDOMServer.renderToString(
<QueryClientProvider client={renderClient}>
<React.Suspense fallback="loading">
<ProfilesComponent />
</React.Suspense>
</QueryClientProvider>,
)

const stringifiedState = JSON.stringify(dehydratedStateServer)
renderClient.clear()
setIsServer(false)

expect(markup).toContain('1')

const el = document.createElement('div')
el.innerHTML = markup

const queryClient = new QueryClient()
hydrate(queryClient, JSON.parse(stringifiedState))

const unmount = ReactHydrate(
<QueryClientProvider client={queryClient}>
<React.Suspense fallback="loading">
<ProfilesComponent />
</React.Suspense>
</QueryClientProvider>,
el,
)

expect(consoleMock).toHaveBeenCalledTimes(0)
expect(el.innerHTML).toContain('1')

unmount()
queryClient.clear()
consoleMock.mockRestore()
})
})
27 changes: 27 additions & 0 deletions packages/react-query/src/__tests__/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useMutationState,
useQueries,
useQuery,
useSuspenseQuery,
} from '..'
import { setIsServer } from './utils'

Expand Down Expand Up @@ -238,6 +239,32 @@ describe('Server Side Rendering', () => {
expect(markup).toContain('data2: data2')
})

it('useSuspenseQuery should suspend on the server instead of exposing undefined data', () => {
const key = queryKey()
const queryFn = vi.fn(() => sleep(10).then(() => [{ id: 1 }]))

function Page() {
const profiles = useSuspenseQuery({
queryKey: key,
queryFn,
}).data

return (
<div>{String(profiles.find((profile) => profile.id === 1)?.id)}</div>
)
}

const markup = renderToString(
<QueryClientProvider client={queryClient}>
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
</QueryClientProvider>,
)

expect(markup).toContain('loading')
})

it('useMutation should return idle status', () => {
function Page() {
const mutation = useMutation({
Expand Down
Loading
Loading