Latest repo

This commit is contained in:
Marc
2025-06-02 16:42:16 +00:00
parent 53ddf1a329
commit cde5fae175
27907 changed files with 3875388 additions and 1 deletions

View File

@@ -0,0 +1,113 @@
/* eslint-disable react-compiler/react-compiler */
'use client'
import * as React from 'react'
import { hydrate } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import type {
DehydratedState,
HydrateOptions,
OmitKeyof,
QueryClient,
} from '@tanstack/query-core'
export interface HydrationBoundaryProps {
state?: unknown
options?: OmitKeyof<HydrateOptions, 'defaultOptions'> & {
defaultOptions?: OmitKeyof<
Exclude<HydrateOptions['defaultOptions'], undefined>,
'mutations'
>
}
children?: React.ReactNode
queryClient?: QueryClient
}
export const HydrationBoundary = ({
children,
options = {},
state,
queryClient,
}: HydrationBoundaryProps) => {
const client = useQueryClient(queryClient)
const [hydrationQueue, setHydrationQueue] = React.useState<
DehydratedState['queries'] | undefined
>()
const optionsRef = React.useRef(options)
optionsRef.current = options
// This useMemo is for performance reasons only, everything inside it _must_
// be safe to run in every render and code here should be read as "in render".
//
// This code needs to happen during the render phase, because after initial
// SSR, hydration needs to happen _before_ children render. Also, if hydrating
// during a transition, we want to hydrate as much as is safe in render so
// we can prerender as much as possible.
//
// For any queries that already exist in the cache, we want to hold back on
// hydrating until _after_ the render phase. The reason for this is that during
// transitions, we don't want the existing queries and observers to update to
// the new data on the current page, only _after_ the transition is committed.
// If the transition is aborted, we will have hydrated any _new_ queries, but
// we throw away the fresh data for any existing ones to avoid unexpectedly
// updating the UI.
React.useMemo(() => {
if (state) {
if (typeof state !== 'object') {
return
}
const queryCache = client.getQueryCache()
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const queries = (state as DehydratedState).queries || []
const newQueries: DehydratedState['queries'] = []
const existingQueries: DehydratedState['queries'] = []
for (const dehydratedQuery of queries) {
const existingQuery = queryCache.get(dehydratedQuery.queryHash)
if (!existingQuery) {
newQueries.push(dehydratedQuery)
} else {
const hydrationIsNewer =
dehydratedQuery.state.dataUpdatedAt >
existingQuery.state.dataUpdatedAt
const queryAlreadyQueued = hydrationQueue?.find(
(query) => query.queryHash === dehydratedQuery.queryHash,
)
if (
hydrationIsNewer &&
(!queryAlreadyQueued ||
dehydratedQuery.state.dataUpdatedAt >
queryAlreadyQueued.state.dataUpdatedAt)
) {
existingQueries.push(dehydratedQuery)
}
}
}
if (newQueries.length > 0) {
// It's actually fine to call this with queries/state that already exists
// in the cache, or is older. hydrate() is idempotent for queries.
hydrate(client, { queries: newQueries }, optionsRef.current)
}
if (existingQueries.length > 0) {
setHydrationQueue((prev) =>
prev ? [...prev, ...existingQueries] : existingQueries,
)
}
}
}, [client, hydrationQueue, state])
React.useEffect(() => {
if (hydrationQueue) {
hydrate(client, { queries: hydrationQueue }, optionsRef.current)
setHydrationQueue(undefined)
}
}, [client, hydrationQueue])
return children as React.ReactElement
}

View File

@@ -0,0 +1,45 @@
'use client'
import * as React from 'react'
import type { QueryClient } from '@tanstack/query-core'
export const QueryClientContext = React.createContext<QueryClient | undefined>(
undefined,
)
export const useQueryClient = (queryClient?: QueryClient) => {
const client = React.useContext(QueryClientContext)
if (queryClient) {
return queryClient
}
if (!client) {
throw new Error('No QueryClient set, use QueryClientProvider to set one')
}
return client
}
export type QueryClientProviderProps = {
client: QueryClient
children?: React.ReactNode
}
export const QueryClientProvider = ({
client,
children,
}: QueryClientProviderProps): React.JSX.Element => {
React.useEffect(() => {
client.mount()
return () => {
client.unmount()
}
}, [client])
return (
<QueryClientContext.Provider value={client}>
{children}
</QueryClientContext.Provider>
)
}

View File

@@ -0,0 +1,56 @@
'use client'
import * as React from 'react'
// CONTEXT
export type QueryErrorResetFunction = () => void
export type QueryErrorIsResetFunction = () => boolean
export type QueryErrorClearResetFunction = () => void
export interface QueryErrorResetBoundaryValue {
clearReset: QueryErrorClearResetFunction
isReset: QueryErrorIsResetFunction
reset: QueryErrorResetFunction
}
function createValue(): QueryErrorResetBoundaryValue {
let isReset = false
return {
clearReset: () => {
isReset = false
},
reset: () => {
isReset = true
},
isReset: () => {
return isReset
},
}
}
const QueryErrorResetBoundaryContext = React.createContext(createValue())
// HOOK
export const useQueryErrorResetBoundary = () =>
React.useContext(QueryErrorResetBoundaryContext)
// COMPONENT
export type QueryErrorResetBoundaryFunction = (
value: QueryErrorResetBoundaryValue,
) => React.ReactNode
export interface QueryErrorResetBoundaryProps {
children: QueryErrorResetBoundaryFunction | React.ReactNode
}
export const QueryErrorResetBoundary = ({
children,
}: QueryErrorResetBoundaryProps) => {
const [value] = React.useState(() => createValue())
return (
<QueryErrorResetBoundaryContext.Provider value={value}>
{typeof children === 'function' ? children(value) : children}
</QueryErrorResetBoundaryContext.Provider>
)
}

View File

@@ -0,0 +1,364 @@
import { beforeAll, describe, expect, test, vi } from 'vitest'
import * as React from 'react'
import { render } from '@testing-library/react'
import * as coreModule from '@tanstack/query-core'
import {
HydrationBoundary,
QueryCache,
QueryClient,
QueryClientProvider,
dehydrate,
useQuery,
} from '..'
import { createQueryClient, sleep } from './utils'
describe('React hydration', () => {
const fetchData: (value: string) => Promise<string> = (value) =>
new Promise((res) => setTimeout(() => res(value), 10))
const dataQuery: (key: [string]) => Promise<string> = (key) =>
fetchData(key[0])
let stringifiedState: string
beforeAll(async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
await queryClient.prefetchQuery({
queryKey: ['string'],
queryFn: () => dataQuery(['stringCached']),
})
const dehydrated = dehydrate(queryClient)
stringifiedState = JSON.stringify(dehydrated)
queryClient.clear()
})
test('should hydrate queries to the cache on context', async () => {
const dehydratedState = JSON.parse(stringifiedState)
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Page() {
const { data } = useQuery({
queryKey: ['string'],
queryFn: () => dataQuery(['string']),
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<Page />
</HydrationBoundary>
</QueryClientProvider>,
)
await rendered.findByText('stringCached')
await rendered.findByText('string')
queryClient.clear()
})
test('should hydrate queries to the cache on custom context', async () => {
const queryCacheOuter = new QueryCache()
const queryCacheInner = new QueryCache()
const queryClientInner = new QueryClient({ queryCache: queryCacheInner })
const queryClientOuter = new QueryClient({ queryCache: queryCacheOuter })
const dehydratedState = JSON.parse(stringifiedState)
function Page() {
const { data } = useQuery({
queryKey: ['string'],
queryFn: () => dataQuery(['string']),
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClientOuter}>
<QueryClientProvider client={queryClientInner}>
<HydrationBoundary state={dehydratedState}>
<Page />
</HydrationBoundary>
</QueryClientProvider>
</QueryClientProvider>,
)
await rendered.findByText('stringCached')
await rendered.findByText('string')
queryClientInner.clear()
queryClientOuter.clear()
})
describe('ReactQueryCacheProvider with hydration support', () => {
test('should hydrate new queries if queries change', async () => {
const dehydratedState = JSON.parse(stringifiedState)
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Page({ queryKey }: { queryKey: [string] }) {
const { data } = useQuery({
queryKey,
queryFn: () => dataQuery(queryKey),
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<Page queryKey={['string']} />
</HydrationBoundary>
</QueryClientProvider>,
)
await rendered.findByText('string')
const intermediateCache = new QueryCache()
const intermediateClient = createQueryClient({
queryCache: intermediateCache,
})
await intermediateClient.prefetchQuery({
queryKey: ['string'],
queryFn: () => dataQuery(['should change']),
})
await intermediateClient.prefetchQuery({
queryKey: ['added'],
queryFn: () => dataQuery(['added']),
})
const dehydrated = dehydrate(intermediateClient)
intermediateClient.clear()
rendered.rerender(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydrated}>
<Page queryKey={['string']} />
<Page queryKey={['added']} />
</HydrationBoundary>
</QueryClientProvider>,
)
// Existing observer should not have updated at this point,
// as that would indicate a side effect in the render phase
rendered.getByText('string')
// New query data should be available immediately
rendered.getByText('added')
await sleep(10)
// After effects phase has had time to run, the observer should have updated
expect(rendered.queryByText('string')).toBeNull()
rendered.getByText('should change')
queryClient.clear()
})
// When we hydrate in transitions that are later aborted, it could be
// confusing to both developers and users if we suddenly updated existing
// state on the screen (why did this update when it was not stale, nothing
// remounted, I didn't change tabs etc?).
// Any queries that does not exist in the cache yet can still be hydrated
// since they don't have any observers on the current page that would update.
test('should hydrate new but not existing queries if transition is aborted', async () => {
const initialDehydratedState = JSON.parse(stringifiedState)
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Page({ queryKey }: { queryKey: [string] }) {
const { data } = useQuery({
queryKey,
queryFn: () => dataQuery(queryKey),
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={initialDehydratedState}>
<Page queryKey={['string']} />
</HydrationBoundary>
</QueryClientProvider>,
)
await rendered.findByText('string')
const intermediateCache = new QueryCache()
const intermediateClient = createQueryClient({
queryCache: intermediateCache,
})
await intermediateClient.prefetchQuery({
queryKey: ['string'],
queryFn: () => dataQuery(['should not change']),
})
await intermediateClient.prefetchQuery({
queryKey: ['added'],
queryFn: () => dataQuery(['added']),
})
const newDehydratedState = dehydrate(intermediateClient)
intermediateClient.clear()
function Thrower() {
throw new Promise(() => {
// Never resolve
})
// @ts-expect-error
return null
}
React.startTransition(() => {
rendered.rerender(
<React.Suspense fallback="loading">
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={newDehydratedState}>
<Page queryKey={['string']} />
<Page queryKey={['added']} />
<Thrower />
</HydrationBoundary>
</QueryClientProvider>
</React.Suspense>,
)
rendered.getByText('loading')
})
React.startTransition(() => {
rendered.rerender(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={initialDehydratedState}>
<Page queryKey={['string']} />
<Page queryKey={['added']} />
</HydrationBoundary>
</QueryClientProvider>,
)
// This query existed before the transition so it should stay the same
rendered.getByText('string')
expect(rendered.queryByText('should not change')).toBeNull()
// New query data should be available immediately because it was
// hydrated in the previous transition, even though the new dehydrated
// state did not contain it
rendered.getByText('added')
})
await sleep(10)
// It should stay the same even after effects have had a chance to run
rendered.getByText('string')
expect(rendered.queryByText('should not change')).toBeNull()
queryClient.clear()
})
test('should hydrate queries to new cache if cache changes', async () => {
const dehydratedState = JSON.parse(stringifiedState)
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Page() {
const { data } = useQuery({
queryKey: ['string'],
queryFn: () => dataQuery(['string']),
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<Page />
</HydrationBoundary>
</QueryClientProvider>,
)
await rendered.findByText('string')
const newClientQueryCache = new QueryCache()
const newClientQueryClient = createQueryClient({
queryCache: newClientQueryCache,
})
rendered.rerender(
<QueryClientProvider client={newClientQueryClient}>
<HydrationBoundary state={dehydratedState}>
<Page />
</HydrationBoundary>
</QueryClientProvider>,
)
await sleep(10)
rendered.getByText('string')
queryClient.clear()
newClientQueryClient.clear()
})
})
test('should not hydrate queries if state is null', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const hydrateSpy = vi.spyOn(coreModule, 'hydrate')
function Page() {
return null
}
render(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={null}>
<Page />
</HydrationBoundary>
</QueryClientProvider>,
)
expect(hydrateSpy).toHaveBeenCalledTimes(0)
hydrateSpy.mockRestore()
queryClient.clear()
})
test('should not hydrate queries if state is undefined', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const hydrateSpy = vi.spyOn(coreModule, 'hydrate')
function Page() {
return null
}
render(
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={undefined}>
<Page />
</HydrationBoundary>
</QueryClientProvider>,
)
expect(hydrateSpy).toHaveBeenCalledTimes(0)
hydrateSpy.mockRestore()
queryClient.clear()
})
})

View File

@@ -0,0 +1,160 @@
import { describe, expect, test, vi } from 'vitest'
import { render, waitFor } from '@testing-library/react'
import { QueryCache, QueryClientProvider, useQuery, useQueryClient } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
describe('QueryClientProvider', () => {
test('sets a specific cache for all queries to use', async () => {
const key = queryKey()
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
return 'test'
},
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)
await waitFor(() => rendered.getByText('test'))
expect(queryCache.find({ queryKey: key })).toBeDefined()
})
test('allows multiple caches to be partitioned', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryCache1 = new QueryCache()
const queryCache2 = new QueryCache()
const queryClient1 = createQueryClient({ queryCache: queryCache1 })
const queryClient2 = createQueryClient({ queryCache: queryCache2 })
function Page1() {
const { data } = useQuery({
queryKey: key1,
queryFn: async () => {
await sleep(10)
return 'test1'
},
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
function Page2() {
const { data } = useQuery({
queryKey: key2,
queryFn: async () => {
await sleep(10)
return 'test2'
},
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<>
<QueryClientProvider client={queryClient1}>
<Page1 />
</QueryClientProvider>
<QueryClientProvider client={queryClient2}>
<Page2 />
</QueryClientProvider>
</>,
)
await waitFor(() => rendered.getByText('test1'))
await waitFor(() => rendered.getByText('test2'))
expect(queryCache1.find({ queryKey: key1 })).toBeDefined()
expect(queryCache1.find({ queryKey: key2 })).not.toBeDefined()
expect(queryCache2.find({ queryKey: key1 })).not.toBeDefined()
expect(queryCache2.find({ queryKey: key2 })).toBeDefined()
})
test("uses defaultOptions for queries when they don't provide their own config", async () => {
const key = queryKey()
const queryCache = new QueryCache()
const queryClient = createQueryClient({
queryCache,
defaultOptions: {
queries: {
gcTime: Infinity,
},
},
})
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
return 'test'
},
})
return (
<div>
<h1>{data}</h1>
</div>
)
}
const rendered = render(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)
await waitFor(() => rendered.getByText('test'))
expect(queryCache.find({ queryKey: key })).toBeDefined()
expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity)
})
describe('useQueryClient', () => {
test('should throw an error if no query client has been set', () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
useQueryClient()
return null
}
expect(() => render(<Page />)).toThrow(
'No QueryClient set, use QueryClientProvider to set one',
)
consoleMock.mockRestore()
})
})
})

View File

@@ -0,0 +1,792 @@
import { describe, expect, it, vi } from 'vitest'
import { fireEvent, waitFor } from '@testing-library/react'
import { ErrorBoundary } from 'react-error-boundary'
import * as React from 'react'
import {
QueryCache,
QueryErrorResetBoundary,
useQueries,
useQuery,
useSuspenseQueries,
useSuspenseQuery,
} from '..'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
describe('QueryErrorResetBoundary', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
describe('useQuery', () => {
it('should retry fetch if the reset error boundary has been reset', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
throwOnError: 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>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
consoleMock.mockRestore()
})
it('should not throw error if query is disabled', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const { data, status } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
enabled: !succeed,
throwOnError: true,
})
return (
<div>
<div>status: {status}</div>
<div>{data}</div>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('status: error'))
consoleMock.mockRestore()
})
it('should not throw error if query is disabled, and refetch if query becomes enabled again', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const [enabled, setEnabled] = React.useState(false)
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
enabled,
throwOnError: true,
})
React.useEffect(() => {
setEnabled(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>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
consoleMock.mockRestore()
})
it('should throw error if query is disabled and manually refetch', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
function Page() {
const { data, refetch, status, fetchStatus } = useQuery<string>({
queryKey: key,
queryFn: async () => {
throw new Error('Error')
},
retry: false,
enabled: false,
throwOnError: true,
})
return (
<div>
<button onClick={() => refetch()}>refetch</button>
<div>
status: {status}, fetchStatus: {fetchStatus}
</div>
<div>{data}</div>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() =>
rendered.getByText('status: pending, fetchStatus: idle'),
)
fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
await waitFor(() => rendered.getByText('error boundary'))
consoleMock.mockRestore()
})
it('should not retry fetch if the reset error boundary has not been reset', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
throwOnError: true,
})
return <div>{data}</div>
}
const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{() => (
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
consoleMock.mockRestore()
})
it('should retry fetch if the reset error boundary has been reset and the query contains data from a previous fetch', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
throwOnError: true,
initialData: 'initial',
})
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>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
consoleMock.mockRestore()
})
it('should not retry fetch if the reset error boundary has not been reset after a previous reset', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
let shouldReset = true
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
throwOnError: true,
})
return <div>{data}</div>
}
const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={() => {
if (shouldReset) {
reset()
}
}}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
shouldReset = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
succeed = true
shouldReset = false
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
consoleMock.mockRestore()
})
it('should throw again on error after the reset error boundary has been reset', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let fetchCount = 0
function Page() {
const { data } = useQuery<string>({
queryKey: key,
queryFn: async () => {
fetchCount++
await sleep(10)
throw new Error('Error')
},
retry: false,
throwOnError: 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>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
expect(fetchCount).toBe(3)
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: async () => {
fetchCount++
await sleep(10)
if (fetchCount > 2) {
return 'data'
} else {
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>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
expect(fetchCount).toBe(3)
expect(renders).toBe(1)
consoleMock.mockRestore()
})
it('should render children', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
return (
<div>
<span>page</span>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
<Page />
</QueryErrorResetBoundary>,
)
expect(rendered.queryByText('page')).not.toBeNull()
consoleMock.mockRestore()
})
it('should show error boundary when using tracked queries even though we do not track the error field', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
throwOnError: 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>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
consoleMock.mockRestore()
})
})
describe('useQueries', () => {
it('should retry fetch if the reset error boundary has been reset', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
const [{ data }] = useQueries({
queries: [
{
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
return 'data'
}
},
retry: false,
throwOnError: true,
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>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
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: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Error')
} else {
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="Loader">
<Page />
</React.Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('data'))
consoleMock.mockRestore()
})
})
})

View File

@@ -0,0 +1,162 @@
import { describe, expect, it, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import * as React from 'react'
import { QueryCache, hashKey } from '@tanstack/query-core'
import { useQuery } from '..'
import {
PERSISTER_KEY_PREFIX,
experimental_createPersister,
} from '../../../query-persist-client-core/src'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
describe('fine grained persister', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
it('should restore query state from persister and not refetch', async () => {
const key = queryKey()
const hash = hashKey(key)
const spy = vi.fn(() => Promise.resolve('Works from queryFn'))
const mapStorage = new Map()
const storage = {
getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)),
setItem: async (itemKey: string, value: unknown) => {
mapStorage.set(itemKey, value)
},
removeItem: async (itemKey: string) => {
mapStorage.delete(itemKey)
},
}
await storage.setItem(
`${PERSISTER_KEY_PREFIX}-${hash}`,
JSON.stringify({
buster: '',
queryHash: hash,
queryKey: key,
state: {
dataUpdatedAt: Date.now(),
data: 'Works from persister',
},
}),
)
function Test() {
const [_, setRef] = React.useState<HTMLDivElement | null>()
const { data } = useQuery({
queryKey: key,
queryFn: spy,
persister: experimental_createPersister({
storage,
}),
staleTime: 5000,
})
return <div ref={(value) => setRef(value)}>{data}</div>
}
const rendered = renderWithClient(queryClient, <Test />)
await waitFor(() => rendered.getByText('Works from persister'))
expect(spy).not.toHaveBeenCalled()
})
it('should restore query state from persister and refetch', async () => {
const key = queryKey()
const hash = hashKey(key)
const spy = vi.fn(async () => {
await sleep(5)
return 'Works from queryFn'
})
const mapStorage = new Map()
const storage = {
getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)),
setItem: async (itemKey: string, value: unknown) => {
mapStorage.set(itemKey, value)
},
removeItem: async (itemKey: string) => {
mapStorage.delete(itemKey)
},
}
await storage.setItem(
`${PERSISTER_KEY_PREFIX}-${hash}`,
JSON.stringify({
buster: '',
queryHash: hash,
queryKey: key,
state: {
dataUpdatedAt: Date.now(),
data: 'Works from persister',
},
}),
)
function Test() {
const [_, setRef] = React.useState<HTMLDivElement | null>()
const { data } = useQuery({
queryKey: key,
queryFn: spy,
persister: experimental_createPersister({
storage,
}),
})
return <div ref={(value) => setRef(value)}>{data}</div>
}
const rendered = renderWithClient(queryClient, <Test />)
await waitFor(() => rendered.getByText('Works from persister'))
await waitFor(() => rendered.getByText('Works from queryFn'))
expect(spy).toHaveBeenCalledTimes(1)
})
it('should store query state to persister after fetch', async () => {
const key = queryKey()
const hash = hashKey(key)
const spy = vi.fn(() => Promise.resolve('Works from queryFn'))
const mapStorage = new Map()
const storage = {
getItem: (itemKey: string) => Promise.resolve(mapStorage.get(itemKey)),
setItem: async (itemKey: string, value: unknown) => {
mapStorage.set(itemKey, value)
},
removeItem: async (itemKey: string) => {
mapStorage.delete(itemKey)
},
}
function Test() {
const [_, setRef] = React.useState<HTMLDivElement | null>()
const { data } = useQuery({
queryKey: key,
queryFn: spy,
persister: experimental_createPersister({
storage,
}),
})
return <div ref={(value) => setRef(value)}>{data}</div>
}
const rendered = renderWithClient(queryClient, <Test />)
await waitFor(() => rendered.getByText('Works from queryFn'))
expect(spy).toHaveBeenCalledTimes(1)
const storedItem = await storage.getItem(`${PERSISTER_KEY_PREFIX}-${hash}`)
expect(JSON.parse(storedItem)).toMatchObject({
state: {
data: 'Works from queryFn',
},
})
})
})

View File

@@ -0,0 +1,203 @@
import { describe, expectTypeOf, it, test } from 'vitest'
import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core'
import { infiniteQueryOptions } from '../infiniteQueryOptions'
import { useInfiniteQuery } from '../useInfiniteQuery'
import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery'
import { useQuery } from '../useQuery'
import type { InfiniteData, InitialDataFunction } from '@tanstack/query-core'
describe('infiniteQueryOptions', () => {
it('should not allow excess properties', () => {
infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('data'),
getNextPageParam: () => 1,
initialPageParam: 1,
// @ts-expect-error this is a good error, because stallTime does not exist!
stallTime: 1000,
})
})
it('should infer types for callbacks', () => {
infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('data'),
staleTime: 1000,
getNextPageParam: () => 1,
initialPageParam: 1,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
},
})
})
it('should work when passed to useInfiniteQuery', () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const { data } = useInfiniteQuery(options)
// known issue: type of pageParams is unknown when returned from useInfiniteQuery
expectTypeOf(data).toEqualTypeOf<
InfiniteData<string, unknown> | undefined
>()
})
it('should work when passed to useSuspenseInfiniteQuery', () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const { data } = useSuspenseInfiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, unknown>>()
})
it('should work when passed to fetchInfiniteQuery', async () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const data = await new QueryClient().fetchInfiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
})
it('should tag the queryKey with the result type of the QueryFn', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<InfiniteData<string>>()
})
it('should tag the queryKey even if no promise is returned', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => 'string',
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<InfiniteData<string>>()
})
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
select: (data) => data.pages,
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<InfiniteData<string>>()
})
it('should return the proper type when passed to getQueryData', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(queryKey)
expectTypeOf(data).toEqualTypeOf<
InfiniteData<string, unknown> | undefined
>()
})
it('should properly type when passed to setQueryData', () => {
const { queryKey } = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
const queryClient = new QueryClient()
const data = queryClient.setQueryData(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<
InfiniteData<string, unknown> | undefined
>()
return prev
})
expectTypeOf(data).toEqualTypeOf<
InfiniteData<string, unknown> | undefined
>()
})
it('should throw a type error when using queryFn with skipToken in a suspense query', () => {
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn:
Math.random() > 0.5 ? skipToken : () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
// @ts-expect-error TS2345
const { data } = useSuspenseInfiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, unknown>>()
})
test('should not be allowed to be passed to non-infinite query functions', () => {
const queryClient = new QueryClient()
const options = infiniteQueryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve('string'),
getNextPageParam: () => 1,
initialPageParam: 1,
})
// @ts-expect-error cannot pass infinite options to non-infinite query functions
useQuery(options)
// @ts-expect-error cannot pass infinite options to non-infinite query functions
queryClient.ensureQueryData(options)
// @ts-expect-error cannot pass infinite options to non-infinite query functions
queryClient.fetchQuery(options)
// @ts-expect-error cannot pass infinite options to non-infinite query functions
queryClient.prefetchQuery(options)
})
test('allow optional initialData function', () => {
const initialData: { example: boolean } | undefined = { example: true }
const queryOptions = infiniteQueryOptions({
queryKey: ['example'],
queryFn: async () => initialData,
initialData: initialData
? () => ({ pages: [initialData], pageParams: [] })
: undefined,
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryOptions.initialData).toMatchTypeOf<
| InitialDataFunction<InfiniteData<{ example: boolean }, number>>
| InfiniteData<{ example: boolean }, number>
| undefined
>()
})
test('allow optional initialData object', () => {
const initialData: { example: boolean } | undefined = { example: true }
const queryOptions = infiniteQueryOptions({
queryKey: ['example'],
queryFn: async () => initialData,
initialData: initialData
? { pages: [initialData], pageParams: [] }
: undefined,
getNextPageParam: () => 1,
initialPageParam: 1,
})
expectTypeOf(queryOptions.initialData).toMatchTypeOf<
| InitialDataFunction<InfiniteData<{ example: boolean }, number>>
| InfiniteData<{ example: boolean }, number>
| undefined
>()
})
})

View File

@@ -0,0 +1,80 @@
import { describe, expectTypeOf, it } from 'vitest'
import { usePrefetchInfiniteQuery, usePrefetchQuery } from '..'
describe('usePrefetchQuery', () => {
it('should return nothing', () => {
const result = usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
expectTypeOf(result).toEqualTypeOf<void>()
})
it('should not allow refetchInterval, enabled or throwOnError options', () => {
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
refetchInterval: 1000,
})
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
enabled: true,
})
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
throwOnError: true,
})
})
})
describe('useInfinitePrefetchQuery', () => {
it('should return nothing', () => {
const result = usePrefetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
})
expectTypeOf(result).toEqualTypeOf<void>()
})
it('should require initialPageParam and getNextPageParam', () => {
// @ts-expect-error TS2345
usePrefetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
})
it('should not allow refetchInterval, enabled or throwOnError options', () => {
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
refetchInterval: 1000,
})
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
enabled: true,
})
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
throwOnError: true,
})
})
})

View File

@@ -0,0 +1,449 @@
import { describe, expect, it, vi } from 'vitest'
import React from 'react'
import { fireEvent, waitFor } from '@testing-library/react'
import { ErrorBoundary } from 'react-error-boundary'
import {
QueryCache,
usePrefetchInfiniteQuery,
usePrefetchQuery,
useQueryErrorResetBoundary,
useSuspenseInfiniteQuery,
useSuspenseQuery,
} from '..'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
import type {
InfiniteData,
UseSuspenseInfiniteQueryOptions,
UseSuspenseQueryOptions,
} from '..'
import type { Mock } from 'vitest'
const generateQueryFn = (data: string) =>
vi
.fn<(...args: Array<any>) => Promise<string>>()
.mockImplementation(async () => {
await sleep(10)
return data
})
const generateInfiniteQueryOptions = (
data: Array<{ data: string; currentPage: number; totalPages: number }>,
) => {
let currentPage = 0
return {
queryFn: vi
.fn<(...args: Array<any>) => Promise<(typeof data)[number]>>()
.mockImplementation(async () => {
const currentPageData = data[currentPage]
if (!currentPageData) {
throw new Error('No data defined for page ' + currentPage)
}
await sleep(10)
currentPage++
return currentPageData
}),
initialPageParam: 1,
getNextPageParam: (lastPage: (typeof data)[number]) =>
lastPage.currentPage === lastPage.totalPages
? undefined
: lastPage.currentPage + 1,
}
}
describe('usePrefetchQuery', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Suspended<TData = unknown>(props: {
queryOpts: UseSuspenseQueryOptions<TData, Error, TData, Array<string>>
children?: React.ReactNode
}) {
const state = useSuspenseQuery(props.queryOpts)
return (
<div>
<div>data: {String(state.data)}</div>
{props.children}
</div>
)
}
it('should prefetch query if query state does not exist', async () => {
const queryOpts = {
queryKey: queryKey(),
queryFn: generateQueryFn('prefetchQuery'),
}
const componentQueryOpts = {
...queryOpts,
queryFn: generateQueryFn('useSuspenseQuery'),
}
function App() {
usePrefetchQuery(queryOpts)
return (
<React.Suspense fallback="Loading...">
<Suspended queryOpts={componentQueryOpts} />
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('data: prefetchQuery'))
expect(queryOpts.queryFn).toHaveBeenCalledTimes(1)
})
it('should not prefetch query if query state exists', async () => {
const queryOpts = {
queryKey: queryKey(),
queryFn: generateQueryFn('The usePrefetchQuery hook is smart!'),
}
function App() {
usePrefetchQuery(queryOpts)
return (
<React.Suspense fallback="Loading...">
<Suspended queryOpts={queryOpts} />
</React.Suspense>
)
}
await queryClient.fetchQuery(queryOpts)
queryOpts.queryFn.mockClear()
const rendered = renderWithClient(queryClient, <App />)
expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument()
await waitFor(() =>
rendered.getByText('data: The usePrefetchQuery hook is smart!'),
)
expect(queryOpts.queryFn).not.toHaveBeenCalled()
})
it('should let errors fall through and not refetch failed queries', async () => {
const consoleMock = vi.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
const queryFn = generateQueryFn('Not an error')
const queryOpts = {
queryKey: queryKey(),
queryFn,
}
queryFn.mockImplementationOnce(async () => {
await sleep(10)
throw new Error('Oops! Server error!')
})
function App() {
usePrefetchQuery(queryOpts)
return (
<ErrorBoundary fallbackRender={() => <div>Oops!</div>}>
<React.Suspense fallback="Loading...">
<Suspended queryOpts={queryOpts} />
</React.Suspense>
</ErrorBoundary>
)
}
await queryClient.prefetchQuery(queryOpts)
queryFn.mockClear()
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('Oops!'))
expect(rendered.queryByText('data: Not an error')).not.toBeInTheDocument()
expect(queryOpts.queryFn).not.toHaveBeenCalled()
consoleMock.mockRestore()
})
it('should not create an endless loop when using inside a suspense boundary', async () => {
const queryFn = generateQueryFn('prefetchedQuery')
const queryOpts = {
queryKey: queryKey(),
queryFn,
}
function Prefetch({ children }: { children: React.ReactNode }) {
usePrefetchQuery(queryOpts)
return <>{children}</>
}
function App() {
return (
<React.Suspense>
<Prefetch>
<Suspended queryOpts={queryOpts} />
</Prefetch>
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('data: prefetchedQuery'))
expect(queryOpts.queryFn).toHaveBeenCalledTimes(1)
})
it('should be able to recover from errors and try fetching again', async () => {
const consoleMock = vi.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
const queryFn = generateQueryFn('This is fine :dog: :fire:')
const queryOpts = {
queryKey: queryKey(),
queryFn,
}
queryFn.mockImplementationOnce(async () => {
await sleep(10)
throw new Error('Oops! Server error!')
})
function App() {
const { reset } = useQueryErrorResetBoundary()
usePrefetchQuery(queryOpts)
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>Oops!</div>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Suspended queryOpts={queryOpts} />
</React.Suspense>
</ErrorBoundary>
)
}
await queryClient.prefetchQuery(queryOpts)
queryFn.mockClear()
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('Oops!'))
fireEvent.click(rendered.getByText('Try again'))
await waitFor(() => rendered.getByText('data: This is fine :dog: :fire:'))
expect(queryOpts.queryFn).toHaveBeenCalledTimes(1)
consoleMock.mockRestore()
})
it('should not create a suspense waterfall if prefetch is fired', async () => {
const firstQueryOpts = {
queryKey: queryKey(),
queryFn: generateQueryFn('Prefetch is nice!'),
}
const secondQueryOpts = {
queryKey: queryKey(),
queryFn: generateQueryFn('Prefetch is really nice!!'),
}
const thirdQueryOpts = {
queryKey: queryKey(),
queryFn: generateQueryFn('Prefetch does not create waterfalls!!'),
}
const Fallback = vi.fn().mockImplementation(() => <div>Loading...</div>)
function App() {
usePrefetchQuery(firstQueryOpts)
usePrefetchQuery(secondQueryOpts)
usePrefetchQuery(thirdQueryOpts)
return (
<React.Suspense fallback={<Fallback />}>
<Suspended queryOpts={firstQueryOpts}>
<Suspended queryOpts={secondQueryOpts}>
<Suspended queryOpts={thirdQueryOpts} />
</Suspended>
</Suspended>
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
expect(
queryClient.getQueryState(firstQueryOpts.queryKey)?.fetchStatus,
).toBe('fetching')
expect(
queryClient.getQueryState(secondQueryOpts.queryKey)?.fetchStatus,
).toBe('fetching')
expect(
queryClient.getQueryState(thirdQueryOpts.queryKey)?.fetchStatus,
).toBe('fetching')
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('data: Prefetch is nice!'))
await waitFor(() => rendered.getByText('data: Prefetch is really nice!!'))
await waitFor(() =>
rendered.getByText('data: Prefetch does not create waterfalls!!'),
)
expect(Fallback).toHaveBeenCalledTimes(1)
expect(firstQueryOpts.queryFn).toHaveBeenCalledTimes(1)
expect(secondQueryOpts.queryFn).toHaveBeenCalledTimes(1)
expect(thirdQueryOpts.queryFn).toHaveBeenCalledTimes(1)
})
})
describe('usePrefetchInfiniteQuery', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const Fallback = vi.fn().mockImplementation(() => <div>Loading...</div>)
function Suspended<T = unknown>(props: {
queryOpts: UseSuspenseInfiniteQueryOptions<
T,
Error,
InfiniteData<T>,
any,
Array<string>,
any
>
renderPage: (page: T) => React.JSX.Element
}) {
const state = useSuspenseInfiniteQuery(props.queryOpts)
return (
<div>
{state.data.pages.map((page, index) => (
<div key={index}>{props.renderPage(page)}</div>
))}
<button onClick={() => state.fetchNextPage()}>Next Page</button>
</div>
)
}
it('should prefetch an infinite query if query state does not exist', async () => {
const data = [
{ data: 'Do you fetch on render?', currentPage: 1, totalPages: 3 },
{ data: 'Or do you render as you fetch?', currentPage: 2, totalPages: 3 },
{
data: 'Either way, Tanstack Query helps you!',
currentPage: 3,
totalPages: 3,
},
]
const queryOpts = {
queryKey: queryKey(),
...generateInfiniteQueryOptions(data),
}
function App() {
usePrefetchInfiniteQuery({ ...queryOpts, pages: data.length })
return (
<React.Suspense fallback={<Fallback />}>
<Suspended
queryOpts={queryOpts}
renderPage={(page) => <div>data: {page.data}</div>}
/>
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('data: Do you fetch on render?'))
fireEvent.click(rendered.getByText('Next Page'))
await waitFor(() =>
rendered.getByText('data: Or do you render as you fetch?'),
)
fireEvent.click(rendered.getByText('Next Page'))
await waitFor(() =>
rendered.getByText('data: Either way, Tanstack Query helps you!'),
)
expect(Fallback).toHaveBeenCalledTimes(1)
expect(queryOpts.queryFn).toHaveBeenCalledTimes(3)
})
it('should not display fallback if the query cache is already populated', async () => {
const queryOpts = {
queryKey: queryKey(),
...generateInfiniteQueryOptions([
{ data: 'Prefetch rocks!', currentPage: 1, totalPages: 3 },
{ data: 'No waterfalls, boy!', currentPage: 2, totalPages: 3 },
{ data: 'Tanstack Query #ftw', currentPage: 3, totalPages: 3 },
]),
}
await queryClient.prefetchInfiniteQuery({ ...queryOpts, pages: 3 })
;(queryOpts.queryFn as Mock).mockClear()
function App() {
usePrefetchInfiniteQuery(queryOpts)
return (
<React.Suspense fallback={<Fallback />}>
<Suspended
queryOpts={queryOpts}
renderPage={(page) => <div>data: {page.data}</div>}
/>
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('data: Prefetch rocks!'))
fireEvent.click(rendered.getByText('Next Page'))
await waitFor(() => rendered.getByText('data: No waterfalls, boy!'))
fireEvent.click(rendered.getByText('Next Page'))
await waitFor(() => rendered.getByText('data: Tanstack Query #ftw'))
expect(queryOpts.queryFn).not.toHaveBeenCalled()
expect(Fallback).not.toHaveBeenCalled()
})
it('should not create an endless loop when using inside a suspense boundary', async () => {
const queryOpts = {
queryKey: queryKey(),
...generateInfiniteQueryOptions([
{ data: 'Infinite Page 1', currentPage: 1, totalPages: 3 },
{ data: 'Infinite Page 2', currentPage: 1, totalPages: 3 },
{ data: 'Infinite Page 3', currentPage: 1, totalPages: 3 },
]),
}
function Prefetch({ children }: { children: React.ReactNode }) {
usePrefetchInfiniteQuery(queryOpts)
return <>{children}</>
}
function App() {
return (
<React.Suspense>
<Prefetch>
<Suspended
queryOpts={queryOpts}
renderPage={(page) => <div>data: {page.data}</div>}
/>
</Prefetch>
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('data: Infinite Page 1'))
fireEvent.click(rendered.getByText('Next Page'))
await waitFor(() => rendered.getByText('data: Infinite Page 2'))
fireEvent.click(rendered.getByText('Next Page'))
await waitFor(() => rendered.getByText('data: Infinite Page 3'))
expect(queryOpts.queryFn).toHaveBeenCalledTimes(3)
})
})

View File

@@ -0,0 +1,236 @@
import { describe, expectTypeOf, it } from 'vitest'
import {
QueriesObserver,
QueryClient,
dataTagSymbol,
skipToken,
} from '@tanstack/query-core'
import { queryOptions } from '../queryOptions'
import { useQuery } from '../useQuery'
import { useQueries } from '../useQueries'
import { useSuspenseQuery } from '../useSuspenseQuery'
import type {
InitialDataFunction,
QueryObserverResult,
} from '@tanstack/query-core'
describe('queryOptions', () => {
it('should not allow excess properties', () => {
queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error this is a good error, because stallTime does not exist!
stallTime: 1000,
})
})
it('should infer types for callbacks', () => {
queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
staleTime: 1000,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
it('should work when passed to useQuery', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const { data } = useQuery(options)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should work when passed to useSuspenseQuery', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const { data } = useSuspenseQuery(options)
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should work when passed to fetchQuery', async () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const data = await new QueryClient().fetchQuery(options)
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should work when passed to useQueries', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const [{ data }] = useQueries({
queries: [options],
})
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should tag the queryKey with the result type of the QueryFn', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<number>()
})
it('should tag the queryKey even if no promise is returned', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => 5,
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<number>()
})
it('should tag the queryKey with unknown if there is no queryFn', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<unknown>()
})
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
select: (data) => data.toString(),
})
expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf<number>()
})
it('should return the proper type when passed to getQueryData', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(queryKey)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should return the proper type when passed to getQueryState', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const state = queryClient.getQueryState(queryKey)
expectTypeOf(state?.data).toEqualTypeOf<number | undefined>()
})
it('should properly type updaterFn when passed to setQueryData', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const data = queryClient.setQueryData(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
return prev
})
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should properly type value when passed to setQueryData', () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
// @ts-expect-error value should be a number
queryClient.setQueryData(queryKey, '5')
// @ts-expect-error value should be a number
queryClient.setQueryData(queryKey, () => '5')
const data = queryClient.setQueryData(queryKey, 5)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should infer even if there is a conditional skipToken', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(options.queryKey)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should infer to unknown if we disable a query with just a skipToken', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: skipToken,
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(options.queryKey)
expectTypeOf(data).toEqualTypeOf<unknown>()
})
it('should throw a type error when using queryFn with skipToken in a suspense query', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
})
// @ts-expect-error TS2345
const { data } = useSuspenseQuery(options)
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should return the proper type when passed to QueriesObserver', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const queriesObserver = new QueriesObserver(queryClient, [options])
expectTypeOf(queriesObserver).toEqualTypeOf<
QueriesObserver<Array<QueryObserverResult>>
>()
})
it('should allow undefined response in initialData', () => {
return (id: string | null) =>
queryOptions({
queryKey: ['todo', id],
queryFn: () =>
Promise.resolve({
id: '1',
title: 'Do Laundry',
}),
initialData: () =>
!id
? undefined
: {
id,
title: 'Initial Data',
},
})
})
it('should allow optional initialData object', () => {
const testFn = (id?: string) => {
const options = queryOptions({
queryKey: ['test'],
queryFn: async () => 'something string',
initialData: id ? 'initial string' : undefined,
})
expectTypeOf(options.initialData).toMatchTypeOf<
InitialDataFunction<string> | string | undefined
>()
}
testFn('id')
testFn()
})
})

View File

@@ -0,0 +1,266 @@
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import { hydrateRoot } from 'react-dom/client'
import { act } from 'react'
import * as ReactDOMServer from 'react-dom/server'
import {
QueryCache,
QueryClientProvider,
dehydrate,
hydrate,
useQuery,
} from '..'
import { createQueryClient, setIsServer, sleep } from './utils'
const ReactHydrate = (element: React.ReactElement, container: Element) => {
let root: any
act(() => {
root = hydrateRoot(container, element)
})
return () => {
root.unmount()
}
}
async function fetchData<TData>(value: TData, ms?: number): Promise<TData> {
await sleep(ms || 1)
return value
}
function PrintStateComponent({ componentName, result }: any): any {
return `${componentName} - status:${result.status} fetching:${result.isFetching} data:${result.data}`
}
describe('Server side rendering with de/rehydration', () => {
let previousIsReactActEnvironment: unknown
beforeAll(() => {
// @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist
previousIsReactActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterAll(() => {
// @ts-expect-error we expect IS_REACT_ACT_ENVIRONMENT to exist
globalThis.IS_REACT_ACT_ENVIRONMENT = previousIsReactActEnvironment
})
it('should not mismatch on success', async () => {
const consoleMock = vi.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
const fetchDataSuccess = vi.fn<typeof fetchData>(fetchData)
// -- Shared part --
function SuccessComponent() {
const result = useQuery({
queryKey: ['success'],
queryFn: () => fetchDataSuccess('success!'),
})
return (
<PrintStateComponent componentName="SuccessComponent" result={result} />
)
}
// -- Server part --
setIsServer(true)
const prefetchCache = new QueryCache()
const prefetchClient = createQueryClient({
queryCache: prefetchCache,
})
await prefetchClient.prefetchQuery({
queryKey: ['success'],
queryFn: () => fetchDataSuccess('success'),
})
const dehydratedStateServer = dehydrate(prefetchClient)
const renderCache = new QueryCache()
const renderClient = createQueryClient({
queryCache: renderCache,
})
hydrate(renderClient, dehydratedStateServer)
const markup = ReactDOMServer.renderToString(
<QueryClientProvider client={renderClient}>
<SuccessComponent />
</QueryClientProvider>,
)
const stringifiedState = JSON.stringify(dehydratedStateServer)
renderClient.clear()
setIsServer(false)
const expectedMarkup =
'SuccessComponent - status:success fetching:true data:success'
expect(markup).toBe(expectedMarkup)
expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
// -- Client part --
const el = document.createElement('div')
el.innerHTML = markup
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
hydrate(queryClient, JSON.parse(stringifiedState))
const unmount = ReactHydrate(
<QueryClientProvider client={queryClient}>
<SuccessComponent />
</QueryClientProvider>,
el,
)
// Check that we have no React hydration mismatches
expect(consoleMock).toHaveBeenCalledTimes(0)
expect(fetchDataSuccess).toHaveBeenCalledTimes(2)
expect(el.innerHTML).toBe(expectedMarkup)
unmount()
queryClient.clear()
consoleMock.mockRestore()
})
it('should not mismatch on error', async () => {
const consoleMock = vi.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
const fetchDataError = vi.fn(() => {
throw new Error('fetchDataError')
})
// -- Shared part --
function ErrorComponent() {
const result = useQuery({
queryKey: ['error'],
queryFn: () => fetchDataError(),
retry: false,
})
return (
<PrintStateComponent componentName="ErrorComponent" result={result} />
)
}
// -- Server part --
setIsServer(true)
const prefetchCache = new QueryCache()
const prefetchClient = createQueryClient({
queryCache: prefetchCache,
})
await prefetchClient.prefetchQuery({
queryKey: ['error'],
queryFn: () => fetchDataError(),
})
const dehydratedStateServer = dehydrate(prefetchClient)
const renderCache = new QueryCache()
const renderClient = createQueryClient({
queryCache: renderCache,
})
hydrate(renderClient, dehydratedStateServer)
const markup = ReactDOMServer.renderToString(
<QueryClientProvider client={renderClient}>
<ErrorComponent />
</QueryClientProvider>,
)
const stringifiedState = JSON.stringify(dehydratedStateServer)
renderClient.clear()
setIsServer(false)
const expectedMarkup =
'ErrorComponent - status:pending fetching:true data:undefined'
expect(markup).toBe(expectedMarkup)
// -- Client part --
const el = document.createElement('div')
el.innerHTML = markup
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
hydrate(queryClient, JSON.parse(stringifiedState))
const unmount = ReactHydrate(
<QueryClientProvider client={queryClient}>
<ErrorComponent />
</QueryClientProvider>,
el,
)
expect(consoleMock).toHaveBeenCalledTimes(0)
expect(fetchDataError).toHaveBeenCalledTimes(2)
expect(el.innerHTML).toBe(expectedMarkup)
await sleep(50)
expect(fetchDataError).toHaveBeenCalledTimes(2)
expect(el.innerHTML).toBe(
'ErrorComponent - status:error fetching:false data:undefined',
)
unmount()
queryClient.clear()
consoleMock.mockRestore()
})
it('should not mismatch on queries that were not prefetched', async () => {
const consoleMock = vi.spyOn(console, 'error')
consoleMock.mockImplementation(() => undefined)
const fetchDataSuccess = vi.fn<typeof fetchData>(fetchData)
// -- Shared part --
function SuccessComponent() {
const result = useQuery({
queryKey: ['success'],
queryFn: () => fetchDataSuccess('success!'),
})
return (
<PrintStateComponent componentName="SuccessComponent" result={result} />
)
}
// -- Server part --
setIsServer(true)
const prefetchClient = createQueryClient()
const dehydratedStateServer = dehydrate(prefetchClient)
const renderClient = createQueryClient()
hydrate(renderClient, dehydratedStateServer)
const markup = ReactDOMServer.renderToString(
<QueryClientProvider client={renderClient}>
<SuccessComponent />
</QueryClientProvider>,
)
const stringifiedState = JSON.stringify(dehydratedStateServer)
renderClient.clear()
setIsServer(false)
const expectedMarkup =
'SuccessComponent - status:pending fetching:true data:undefined'
expect(markup).toBe(expectedMarkup)
// -- Client part --
const el = document.createElement('div')
el.innerHTML = markup
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
hydrate(queryClient, JSON.parse(stringifiedState))
const unmount = ReactHydrate(
<QueryClientProvider client={queryClient}>
<SuccessComponent />
</QueryClientProvider>,
el,
)
// Check that we have no React hydration mismatches
expect(consoleMock).toHaveBeenCalledTimes(0)
expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
expect(el.innerHTML).toBe(expectedMarkup)
await sleep(50)
expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
expect(el.innerHTML).toBe(
'SuccessComponent - status:success fetching:false data:success!',
)
unmount()
queryClient.clear()
consoleMock.mockRestore()
})
})

View File

@@ -0,0 +1,158 @@
import * as React from 'react'
import { renderToString } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { QueryCache, QueryClientProvider, useInfiniteQuery, useQuery } from '..'
import { createQueryClient, queryKey, setIsServer, sleep } from './utils'
describe('Server Side Rendering', () => {
setIsServer(true)
it('should not trigger fetch', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const key = queryKey()
const queryFn = vi.fn().mockReturnValue('data')
function Page() {
const query = useQuery({ queryKey: key, queryFn })
const content = `status ${query.status}`
return (
<div>
<div>{content}</div>
</div>
)
}
const markup = renderToString(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)
expect(markup).toContain('status pending')
expect(queryFn).toHaveBeenCalledTimes(0)
queryCache.clear()
})
it('should add prefetched data to cache', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const key = queryKey()
const fetchFn = () => Promise.resolve('data')
const data = await queryClient.fetchQuery({
queryKey: key,
queryFn: fetchFn,
})
expect(data).toBe('data')
expect(queryCache.find({ queryKey: key })?.state.data).toBe('data')
queryCache.clear()
})
it('should return existing data from the cache', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const key = queryKey()
const queryFn = vi.fn(() => {
sleep(10)
return 'data'
})
function Page() {
const query = useQuery({ queryKey: key, queryFn })
const content = `status ${query.status}`
return (
<div>
<div>{content}</div>
</div>
)
}
await queryClient.prefetchQuery({ queryKey: key, queryFn })
const markup = renderToString(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)
expect(markup).toContain('status success')
expect(queryFn).toHaveBeenCalledTimes(1)
queryCache.clear()
})
it('should add initialData to the cache', () => {
const key = queryKey()
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
function Page() {
const [page, setPage] = React.useState(1)
const { data } = useQuery({
queryKey: [key, page],
queryFn: async () => page,
initialData: 1,
})
return (
<div>
<h1 data-testid="title">{data}</h1>
<button onClick={() => setPage(page + 1)}>next</button>
</div>
)
}
renderToString(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)
const keys = queryCache.getAll().map((query) => query.queryKey)
expect(keys).toEqual([[key, 1]])
queryCache.clear()
})
it('useInfiniteQuery should return the correct state', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const key = queryKey()
const queryFn = vi.fn(async () => {
await sleep(5)
return 'page 1'
})
function Page() {
const query = useInfiniteQuery({
queryKey: key,
queryFn,
getNextPageParam: () => undefined,
initialPageParam: 0,
})
return (
<ul>{query.data?.pages.map((page) => <li key={page}>{page}</li>)}</ul>
)
}
await queryClient.prefetchInfiniteQuery({
queryKey: key,
queryFn,
initialPageParam: 0,
})
const markup = renderToString(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)
expect(markup).toContain('page 1')
expect(queryFn).toHaveBeenCalledTimes(1)
queryCache.clear()
})
})

View File

@@ -0,0 +1,142 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { useInfiniteQuery } from '../useInfiniteQuery'
import type { InfiniteData } from '@tanstack/query-core'
describe('pageParam', () => {
it('initialPageParam should define type of param passed to queryFunctionContext', () => {
useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
},
initialPageParam: 1,
getNextPageParam: () => undefined,
})
})
it('direction should be passed to queryFn of useInfiniteQuery', () => {
useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ direction }) => {
expectTypeOf(direction).toEqualTypeOf<'forward' | 'backward'>()
},
initialPageParam: 1,
getNextPageParam: () => undefined,
})
})
it('initialPageParam should define type of param passed to queryFunctionContext for fetchInfiniteQuery', () => {
const queryClient = new QueryClient()
queryClient.fetchInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
},
initialPageParam: 1,
})
})
it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => {
const queryClient = new QueryClient()
queryClient.prefetchInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
},
initialPageParam: 1,
})
})
})
describe('select', () => {
it('should still return paginated data if no select result', () => {
const infiniteQuery = useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
})
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
InfiniteData<number, unknown> | undefined
>()
})
it('should be able to transform data to arbitrary result', () => {
const infiniteQuery = useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, number>>()
return 'selected' as const
},
})
expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>()
})
})
describe('getNextPageParam / getPreviousPageParam', () => {
it('should get typed params', () => {
const infiniteQuery = useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
return String(pageParam)
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
expectTypeOf(lastPage).toEqualTypeOf<string>()
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
expectTypeOf(lastPageParam).toEqualTypeOf<number>()
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
return undefined
},
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
) => {
expectTypeOf(firstPage).toEqualTypeOf<string>()
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
expectTypeOf(firstPageParam).toEqualTypeOf<number>()
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
return undefined
},
})
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
InfiniteData<string, unknown> | undefined
>()
})
})
describe('error booleans', () => {
it('should not be permanently `false`', () => {
const {
isFetchNextPageError,
isFetchPreviousPageError,
isLoadingError,
isRefetchError,
} = useInfiniteQuery({
queryKey: ['key'],
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
})
expectTypeOf(isFetchNextPageError).toEqualTypeOf<boolean>()
expectTypeOf(isFetchPreviousPageError).toEqualTypeOf<boolean>()
expectTypeOf(isLoadingError).toEqualTypeOf<boolean>()
expectTypeOf(isRefetchError).toEqualTypeOf<boolean>()
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,232 @@
import { describe, expect, it } from 'vitest'
import { fireEvent, render, waitFor } from '@testing-library/react'
import * as React from 'react'
import { QueryCache, useIsFetching, useQuery } from '..'
import {
createQueryClient,
queryKey,
renderWithClient,
setActTimeout,
sleep,
} from './utils'
describe('useIsFetching', () => {
// See https://github.com/tannerlinsley/react-query/issues/105
it('should update as queries start and stop fetching', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const key = queryKey()
function IsFetching() {
const isFetching = useIsFetching()
return <div>isFetching: {isFetching}</div>
}
function Query() {
const [ready, setReady] = React.useState(false)
useQuery({
queryKey: key,
queryFn: async () => {
await sleep(50)
return 'test'
},
enabled: ready,
})
return <button onClick={() => setReady(true)}>setReady</button>
}
function Page() {
return (
<div>
<IsFetching />
<Query />
</div>
)
}
const { findByText, getByRole } = renderWithClient(queryClient, <Page />)
await findByText('isFetching: 0')
fireEvent.click(getByRole('button', { name: /setReady/i }))
await findByText('isFetching: 1')
await findByText('isFetching: 0')
})
it('should not update state while rendering', async () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
const key1 = queryKey()
const key2 = queryKey()
const isFetchingArray: Array<number> = []
function IsFetching() {
const isFetching = useIsFetching()
isFetchingArray.push(isFetching)
return null
}
function FirstQuery() {
useQuery({
queryKey: key1,
queryFn: async () => {
await sleep(100)
return 'data'
},
})
return null
}
function SecondQuery() {
useQuery({
queryKey: key2,
queryFn: async () => {
await sleep(100)
return 'data'
},
})
return null
}
function Page() {
const [renderSecond, setRenderSecond] = React.useState(false)
React.useEffect(() => {
setActTimeout(() => {
setRenderSecond(true)
}, 50)
}, [])
return (
<>
<IsFetching />
<FirstQuery />
{renderSecond && <SecondQuery />}
</>
)
}
renderWithClient(queryClient, <Page />)
await waitFor(() => expect(isFetchingArray).toEqual([0, 1, 1, 2, 1, 0]))
})
it('should be able to filter', async () => {
const queryClient = createQueryClient()
const key1 = queryKey()
const key2 = queryKey()
const isFetchingArray: Array<number> = []
function One() {
useQuery({
queryKey: key1,
queryFn: async () => {
await sleep(10)
return 'test'
},
})
return null
}
function Two() {
useQuery({
queryKey: key2,
queryFn: async () => {
await sleep(20)
return 'test'
},
})
return null
}
function Page() {
const [started, setStarted] = React.useState(false)
const isFetching = useIsFetching({ queryKey: key1 })
isFetchingArray.push(isFetching)
return (
<div>
<button onClick={() => setStarted(true)}>setStarted</button>
<div>isFetching: {isFetching}</div>
{started ? (
<>
<One />
<Two />
</>
) : null}
</div>
)
}
const { findByText, getByRole } = renderWithClient(queryClient, <Page />)
await findByText('isFetching: 0')
fireEvent.click(getByRole('button', { name: /setStarted/i }))
await findByText('isFetching: 1')
await findByText('isFetching: 0')
// at no point should we have isFetching: 2
expect(isFetchingArray).toEqual(expect.not.arrayContaining([2]))
})
it('should show the correct fetching state when mounted after a query', async () => {
const queryClient = createQueryClient()
const key = queryKey()
function Page() {
useQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
return 'test'
},
})
const isFetching = useIsFetching()
return (
<div>
<div>isFetching: {isFetching}</div>
</div>
)
}
const rendered = renderWithClient(queryClient, <Page />)
await rendered.findByText('isFetching: 1')
await rendered.findByText('isFetching: 0')
})
it('should use provided custom queryClient', async () => {
const queryClient = createQueryClient()
const key = queryKey()
function Page() {
useQuery(
{
queryKey: key,
queryFn: async () => {
await sleep(10)
return 'test'
},
},
queryClient,
)
const isFetching = useIsFetching({}, queryClient)
return (
<div>
<div>isFetching: {isFetching}</div>
</div>
)
}
const rendered = render(<Page></Page>)
await waitFor(() => rendered.getByText('isFetching: 1'))
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,263 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { fireEvent, render, waitFor } from '@testing-library/react'
import * as React from 'react'
import { useIsMutating, useMutationState } from '../useMutationState'
import { useMutation } from '../useMutation'
import {
createQueryClient,
doNotExecute,
renderWithClient,
sleep,
} from './utils'
import type { MutationState, MutationStatus } from '@tanstack/query-core'
describe('useIsMutating', () => {
it('should return the number of fetching mutations', async () => {
const isMutatingArray: Array<number> = []
const queryClient = createQueryClient()
function IsMutating() {
const isMutating = useIsMutating()
isMutatingArray.push(isMutating)
return null
}
function Mutations() {
const { mutate: mutate1 } = useMutation({
mutationKey: ['mutation1'],
mutationFn: async () => {
await sleep(50)
return 'data'
},
})
const { mutate: mutate2 } = useMutation({
mutationKey: ['mutation2'],
mutationFn: async () => {
await sleep(10)
return 'data'
},
})
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
function Page() {
return (
<div>
<IsMutating />
<Mutations />
</div>
)
}
const rendered = renderWithClient(queryClient, <Page />)
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
await sleep(10)
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
// we don't really care if this yields
// [ +0, 1, 2, +0 ]
// or
// [ +0, 1, 2, 1, +0 ]
// our batching strategy might yield different results
await waitFor(() => expect(isMutatingArray[0]).toEqual(0))
await waitFor(() => expect(isMutatingArray[1]).toEqual(1))
await waitFor(() => expect(isMutatingArray[2]).toEqual(2))
await waitFor(() =>
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0),
)
})
it('should filter correctly by mutationKey', async () => {
const isMutatingArray: Array<number> = []
const queryClient = createQueryClient()
function IsMutating() {
const isMutating = useIsMutating({ mutationKey: ['mutation1'] })
isMutatingArray.push(isMutating)
return null
}
function Page() {
const { mutate: mutate1 } = useMutation({
mutationKey: ['mutation1'],
mutationFn: async () => {
await sleep(100)
return 'data'
},
})
const { mutate: mutate2 } = useMutation({
mutationKey: ['mutation2'],
mutationFn: async () => {
await sleep(100)
return 'data'
},
})
React.useEffect(() => {
mutate1()
mutate2()
}, [mutate1, mutate2])
return <IsMutating />
}
renderWithClient(queryClient, <Page />)
await waitFor(() => expect(isMutatingArray).toEqual([0, 1, 0]))
})
it('should filter correctly by predicate', async () => {
const isMutatingArray: Array<number> = []
const queryClient = createQueryClient()
function IsMutating() {
const isMutating = useIsMutating({
predicate: (mutation) =>
mutation.options.mutationKey?.[0] === 'mutation1',
})
isMutatingArray.push(isMutating)
return null
}
function Page() {
const { mutate: mutate1 } = useMutation({
mutationKey: ['mutation1'],
mutationFn: async () => {
await sleep(100)
return 'data'
},
})
const { mutate: mutate2 } = useMutation({
mutationKey: ['mutation2'],
mutationFn: async () => {
await sleep(100)
return 'data'
},
})
React.useEffect(() => {
mutate1()
mutate2()
}, [mutate1, mutate2])
return <IsMutating />
}
renderWithClient(queryClient, <Page />)
await waitFor(() => expect(isMutatingArray).toEqual([0, 1, 0]))
})
it('should use provided custom queryClient', async () => {
const queryClient = createQueryClient()
function Page() {
const isMutating = useIsMutating({}, queryClient)
const { mutate } = useMutation(
{
mutationKey: ['mutation1'],
mutationFn: async () => {
await sleep(10)
return 'data'
},
},
queryClient,
)
React.useEffect(() => {
mutate()
}, [mutate])
return (
<div>
<div>mutating: {isMutating}</div>
</div>
)
}
const rendered = render(<Page></Page>)
await waitFor(() => rendered.getByText('mutating: 1'))
})
})
describe('useMutationState', () => {
describe('types', () => {
it('should default to QueryState', () => {
doNotExecute(() => {
const result = useMutationState({
filters: { status: 'pending' },
})
expectTypeOf(result).toEqualTypeOf<Array<MutationState>>()
})
})
it('should infer with select', () => {
doNotExecute(() => {
const result = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.status,
})
expectTypeOf(result).toEqualTypeOf<Array<MutationStatus>>()
})
})
})
it('should return variables after calling mutate', async () => {
const queryClient = createQueryClient()
const variables: Array<Array<unknown>> = []
const mutationKey = ['mutation']
function Variables() {
variables.push(
useMutationState({
filters: { mutationKey, status: 'pending' },
select: (mutation) => mutation.state.variables,
}),
)
return null
}
function Mutate() {
const { mutate, data } = useMutation({
mutationKey,
mutationFn: async (input: number) => {
await sleep(150)
return 'data' + input
},
})
return (
<div>
data: {data ?? 'null'}
<button onClick={() => mutate(1)}>mutate</button>
</div>
)
}
function Page() {
return (
<div>
<Variables />
<Mutate />
</div>
)
}
const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('data: null'))
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await waitFor(() => rendered.getByText('data: data1'))
expect(variables).toEqual([[], [1], []])
})
})

View File

@@ -0,0 +1,144 @@
import { describe, expectTypeOf, it } from 'vitest'
import { skipToken } from '..'
import { useQueries } from '../useQueries'
import { queryOptions } from '../queryOptions'
import type { OmitKeyof } from '..'
import type { UseQueryOptions, UseQueryResult } from '../types'
describe('UseQueries config object overload', () => {
it('TData should always be defined when initialData is provided as an object', () => {
const query1 = {
queryKey: ['key1'],
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: false,
},
}
const query2 = {
queryKey: ['key2'],
queryFn: () => 'Query Data',
initialData: 'initial data',
}
const query3 = {
queryKey: ['key2'],
queryFn: () => 'Query Data',
}
const queryResults = useQueries({ queries: [query1, query2, query3] })
const query1Data = queryResults[0].data
const query2Data = queryResults[1].data
const query3Data = queryResults[2].data
expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>()
expectTypeOf(query2Data).toEqualTypeOf<string>()
expectTypeOf(query3Data).toEqualTypeOf<string | undefined>()
})
it('TData should be defined when passed through queryOptions', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: true,
},
})
const queryResults = useQueries({ queries: [options] })
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => {
const query1 = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
select: (data) => data > 1,
})
const query2 = {
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
select: (data: number) => data > 1,
}
const queryResults = useQueries({ queries: [query1, query2] })
const query1Data = queryResults[0].data
const query2Data = queryResults[1].data
expectTypeOf(query1Data).toEqualTypeOf<boolean | undefined>()
expectTypeOf(query2Data).toEqualTypeOf<boolean | undefined>()
})
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
const queryResults = useQueries({
queries: [
{
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
},
],
})
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
describe('custom hook', () => {
it('should allow custom hooks using UseQueryOptions', () => {
type Data = string
const useCustomQueries = (
options?: OmitKeyof<UseQueryOptions<Data>, 'queryKey' | 'queryFn'>,
) => {
return useQueries({
queries: [
{
...options,
queryKey: ['todos-key'],
queryFn: () => Promise.resolve('data'),
},
],
})
}
const queryResults = useCustomQueries()
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<Data | undefined>()
})
})
it('TData should have correct type when conditional skipToken is passed', () => {
const queryResults = useQueries({
queries: [
{
queryKey: ['withSkipToken'],
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
},
],
})
const firstResult = queryResults[0]
expectTypeOf(firstResult).toEqualTypeOf<UseQueryResult<number, Error>>()
expectTypeOf(firstResult.data).toEqualTypeOf<number | undefined>()
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,141 @@
import { describe, expectTypeOf, it } from 'vitest'
import { useQuery } from '../useQuery'
import { queryOptions } from '../queryOptions'
import type { OmitKeyof } from '..'
import type { UseQueryOptions } from '../types'
describe('initialData', () => {
describe('Config object overload', () => {
it('TData should always be defined when initialData is provided as an object', () => {
const { data } = useQuery({
queryKey: ['key'],
queryFn: () => ({ wow: true }),
initialData: { wow: true },
})
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should be defined when passed through queryOptions', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: true,
},
})
const { data } = useQuery(options)
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
})
const query = useQuery({
...options,
select: (data) => data > 1,
})
expectTypeOf(query.data).toEqualTypeOf<boolean | undefined>()
})
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
const { data } = useQuery({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => ({
wow: true,
}),
})
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should have undefined in the union when initialData is NOT provided', () => {
const { data } = useQuery({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
})
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
const { data } = useQuery({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
})
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => {
const { data, isSuccess } = useQuery({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
})
if (isSuccess) {
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
}
})
})
describe('custom hook', () => {
it('should allow custom hooks using UseQueryOptions', () => {
type Data = string
const useCustomQuery = (
options?: OmitKeyof<UseQueryOptions<Data>, 'queryKey' | 'queryFn'>,
) => {
return useQuery({
...options,
queryKey: ['todos-key'],
queryFn: () => Promise.resolve('data'),
})
}
const { data } = useCustomQuery()
expectTypeOf(data).toEqualTypeOf<Data | undefined>()
})
})
describe('structuralSharing', () => {
it('should restrict to same types', () => {
useQuery({
queryKey: ['key'],
queryFn: () => 5,
structuralSharing: (_oldData, newData) => {
return newData
},
})
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
import { describe, expectTypeOf, it } from 'vitest'
import { skipToken } from '@tanstack/query-core'
import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery'
import type { InfiniteData } from '@tanstack/query-core'
describe('useSuspenseInfiniteQuery', () => {
it('should always have data defined', () => {
const { data } = useSuspenseInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
})
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, unknown>>()
})
it('should not allow skipToken in queryFn', () => {
useSuspenseInfiniteQuery({
queryKey: ['key'],
// @ts-expect-error
queryFn: skipToken,
})
useSuspenseInfiniteQuery({
queryKey: ['key'],
// @ts-expect-error
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
})
})
it('should not have pending status', () => {
const { status } = useSuspenseInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
})
expectTypeOf(status).toEqualTypeOf<'error' | 'success'>()
})
it('should not allow placeholderData, enabled or throwOnError props', () => {
useSuspenseInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
// @ts-expect-error TS2345
placeholderData: 5,
enabled: true,
})
useSuspenseInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
// @ts-expect-error TS2345
enabled: true,
})
useSuspenseInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
// @ts-expect-error TS2345
throwOnError: true,
})
})
it('should not return isPlaceholderData', () => {
const query = useSuspenseInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
})
// @ts-expect-error TS2339
query.isPlaceholderData
})
})

View File

@@ -0,0 +1,157 @@
import { describe, expectTypeOf, it } from 'vitest'
import { skipToken, useSuspenseQueries } from '..'
import { queryOptions } from '../queryOptions'
import type { OmitKeyof } from '..'
import type { UseQueryOptions, UseSuspenseQueryResult } from '../types'
describe('UseSuspenseQueries config object overload', () => {
it('TData should always be defined', () => {
const query1 = {
queryKey: ['key1'],
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: false,
},
}
const query2 = {
queryKey: ['key2'],
queryFn: () => 'Query Data',
}
const queryResults = useSuspenseQueries({ queries: [query1, query2] })
const query1Data = queryResults[0].data
const query2Data = queryResults[1].data
expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>()
expectTypeOf(query2Data).toEqualTypeOf<string>()
})
it('TData should be defined when passed through queryOptions', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
})
const queryResults = useSuspenseQueries({ queries: [options] })
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => {
const query1 = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
select: (data) => data > 1,
})
const query2 = {
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
select: (data: number) => data > 1,
}
const queryResults = useSuspenseQueries({ queries: [query1, query2] })
const query1Data = queryResults[0].data
const query2Data = queryResults[1].data
expectTypeOf(query1Data).toEqualTypeOf<boolean>()
expectTypeOf(query2Data).toEqualTypeOf<boolean>()
})
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
const queryResults = useSuspenseQueries({
queries: [
{
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
},
],
})
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('should not allow skipToken in queryFn', () => {
useSuspenseQueries({
queries: [
{
queryKey: ['key'],
// @ts-expect-error
queryFn: skipToken,
},
],
})
useSuspenseQueries({
queries: [
{
queryKey: ['key'],
// @ts-expect-error
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
},
],
})
})
it('TData should have correct type when conditional skipToken is passed', () => {
const queryResults = useSuspenseQueries({
queries: [
{
queryKey: ['withSkipToken'],
// @ts-expect-error
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
},
],
})
const firstResult = queryResults[0]
expectTypeOf(firstResult).toEqualTypeOf<
UseSuspenseQueryResult<number, Error>
>()
expectTypeOf(firstResult.data).toEqualTypeOf<number>()
})
describe('custom hook', () => {
it('should allow custom hooks using UseQueryOptions', () => {
type Data = string
const useCustomQueries = (
options?: OmitKeyof<UseQueryOptions<Data>, 'queryKey' | 'queryFn'>,
) => {
return useSuspenseQueries({
queries: [
{
...options,
queryKey: ['todos-key'],
queryFn: () => Promise.resolve('data'),
},
],
})
}
const queryResults = useCustomQueries()
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<Data>()
})
})
})

View File

@@ -0,0 +1,661 @@
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from 'vitest'
import { act, fireEvent, render, waitFor } from '@testing-library/react'
import * as React from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { useSuspenseQueries, useSuspenseQuery } from '..'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
import type { UseSuspenseQueryOptions } from '..'
type NumberQueryOptions = UseSuspenseQueryOptions<number>
const QUERY_DURATION = 1000
const createQuery: (id: number) => NumberQueryOptions = (id) => ({
queryKey: [id],
queryFn: async () => {
await sleep(QUERY_DURATION)
return id
},
})
const resolveQueries = () => vi.advanceTimersByTimeAsync(QUERY_DURATION)
const queryClient = createQueryClient()
describe('useSuspenseQueries', () => {
const onSuspend = vi.fn()
const onQueriesResolution = vi.fn()
beforeAll(() => {
vi.useFakeTimers()
})
afterAll(() => {
vi.useRealTimers()
})
afterEach(() => {
queryClient.clear()
onSuspend.mockClear()
onQueriesResolution.mockClear()
})
function SuspenseFallback() {
React.useEffect(() => {
onSuspend()
}, [])
return null
}
const withSuspenseWrapper = <T extends object>(Component: React.FC<T>) => {
function SuspendedComponent(props: T) {
return (
<React.Suspense fallback={<SuspenseFallback />}>
<Component {...props} />
</React.Suspense>
)
}
return SuspendedComponent
}
function QueriesContainer({
queries,
}: {
queries: Array<NumberQueryOptions>
}) {
const queriesResults = useSuspenseQueries(
{ queries, combine: (results) => results.map((r) => r.data) },
queryClient,
)
React.useEffect(() => {
onQueriesResolution(queriesResults)
}, [queriesResults])
return null
}
const TestComponent = withSuspenseWrapper(QueriesContainer)
it('should suspend on mount', () => {
render(<TestComponent queries={[1, 2].map(createQuery)} />)
expect(onSuspend).toHaveBeenCalledOnce()
})
it('should resolve queries', async () => {
render(<TestComponent queries={[1, 2].map(createQuery)} />)
await act(resolveQueries)
expect(onQueriesResolution).toHaveBeenCalledTimes(1)
expect(onQueriesResolution).toHaveBeenLastCalledWith([1, 2])
})
it('should not suspend on mount if query has been already fetched', () => {
const query = createQuery(1)
queryClient.setQueryData(query.queryKey, query.queryFn)
render(<TestComponent queries={[query]} />)
expect(onSuspend).not.toHaveBeenCalled()
})
it('should not break suspense when queries change without resolving', async () => {
const initQueries = [1, 2].map(createQuery)
const nextQueries = [3, 4, 5, 6].map(createQuery)
const { rerender } = render(<TestComponent queries={initQueries} />)
rerender(<TestComponent queries={nextQueries} />)
await act(resolveQueries)
expect(onSuspend).toHaveBeenCalledTimes(1)
expect(onQueriesResolution).toHaveBeenCalledTimes(1)
expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6])
})
it('should suspend only once per queries change', async () => {
const initQueries = [1, 2].map(createQuery)
const nextQueries = [3, 4, 5, 6].map(createQuery)
const { rerender } = render(<TestComponent queries={initQueries} />)
await act(resolveQueries)
rerender(<TestComponent queries={nextQueries} />)
await act(resolveQueries)
expect(onSuspend).toHaveBeenCalledTimes(2)
expect(onQueriesResolution).toHaveBeenCalledTimes(2)
expect(onQueriesResolution).toHaveBeenLastCalledWith([3, 4, 5, 6])
})
it('should only call combine after resolving', async () => {
const spy = vi.fn()
const key = queryKey()
function Page() {
const data = useSuspenseQueries({
queries: [1, 2, 3].map((value) => ({
queryKey: [...key, { value }],
queryFn: async () => {
await sleep(value * 10)
return { value: value * 10 }
},
})),
combine: (result) => {
spy(result)
return 'data'
},
})
return <h1>{data}</h1>
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading...">
<Page />
</React.Suspense>,
)
await act(() => vi.advanceTimersByTimeAsync(10))
rendered.getByText('loading...')
expect(spy).not.toHaveBeenCalled()
await act(() => vi.advanceTimersByTimeAsync(30))
rendered.getByText('data')
expect(spy).toHaveBeenCalled()
})
})
describe('useSuspenseQueries 2', () => {
it('should suspend all queries in parallel', async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: Array<string> = []
function Fallback() {
results.push('loading')
return <div>loading</div>
}
function Page() {
const result = useSuspenseQueries({
queries: [
{
queryKey: key1,
queryFn: async () => {
results.push('1')
await sleep(10)
return '1'
},
},
{
queryKey: key2,
queryFn: async () => {
results.push('2')
await sleep(20)
return '2'
},
},
],
})
return (
<div>
<h1>data: {result.map((item) => item.data ?? 'null').join(',')}</h1>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback={<Fallback />}>
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('loading'))
await waitFor(() => rendered.getByText('data: 1,2'))
expect(results).toEqual(['1', '2', 'loading'])
})
it("shouldn't unmount before all promises fetched", async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: Array<string> = []
const refs: Array<number> = []
function Fallback() {
results.push('loading')
return <div>loading</div>
}
function Page() {
const ref = React.useRef(Math.random())
const result = useSuspenseQueries({
queries: [
{
queryKey: key1,
queryFn: async () => {
refs.push(ref.current)
results.push('1')
await sleep(10)
return '1'
},
},
{
queryKey: key2,
queryFn: async () => {
refs.push(ref.current)
results.push('2')
await sleep(20)
return '2'
},
},
],
})
return (
<div>
<h1>data: {result.map((item) => item.data ?? 'null').join(',')}</h1>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback={<Fallback />}>
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('loading'))
expect(refs.length).toBe(2)
await waitFor(() => rendered.getByText('data: 1,2'))
expect(refs[0]).toBe(refs[1])
})
// this addresses the following issue:
// https://github.com/TanStack/query/issues/6344
it('should suspend on offline when query changes, and data should not be undefined', async () => {
function Page() {
const [id, setId] = React.useState(0)
const { data } = useSuspenseQuery({
queryKey: [id],
queryFn: () => Promise.resolve(`Data ${id}`),
})
// defensive guard here
if (data === undefined) {
throw new Error('data cannot be undefined')
}
return (
<>
<div>{data}</div>
<button onClick={() => setId(id + 1)}>fetch</button>
</>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback={<div>loading</div>}>
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('loading'))
await waitFor(() => rendered.getByText('Data 0'))
// go offline
document.dispatchEvent(new CustomEvent('offline'))
fireEvent.click(rendered.getByText('fetch'))
await waitFor(() => rendered.getByText('Data 0'))
// go back online
document.dispatchEvent(new CustomEvent('online'))
fireEvent.click(rendered.getByText('fetch'))
// query should resume
await waitFor(() => rendered.getByText('Data 1'))
})
it('should throw error when queryKey changes and new query fails', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
function Page() {
const [fail, setFail] = React.useState(false)
const { data } = useSuspenseQuery({
queryKey: [key, fail],
queryFn: async () => {
await sleep(10)
if (fail) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retry: 0,
})
return (
<div>
<button onClick={() => setFail(true)}>trigger fail</button>
<div>rendered: {String(data)}</div>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
<React.Suspense fallback={'Loading...'}>
<Page />
</React.Suspense>
</ErrorBoundary>,
)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('rendered: data'))
fireEvent.click(rendered.getByText('trigger fail'))
await waitFor(() => rendered.getByText('error boundary'))
expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual(
new Error('Suspense Error Bingo'),
)
consoleMock.mockRestore()
})
it('should keep previous data when wrapped in a transition', async () => {
const key = queryKey()
function Page() {
const [count, setCount] = React.useState(0)
const [isPending, startTransition] = React.useTransition()
const { data } = useSuspenseQuery({
queryKey: [key, count],
queryFn: async () => {
await sleep(10)
return 'data' + count
},
})
return (
<div>
<button onClick={() => startTransition(() => setCount(count + 1))}>
inc
</button>
<div>{isPending ? 'Pending...' : String(data)}</div>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback={'Loading...'}>
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('data0'))
fireEvent.click(rendered.getByText('inc'))
await waitFor(() => rendered.getByText('Pending...'))
await waitFor(() => rendered.getByText('data1'))
})
it('should not request old data inside transitions (issue #6486)', async () => {
const key = queryKey()
let queryFnCount = 0
function App() {
const [count, setCount] = React.useState(0)
return (
<div>
<button
onClick={() => React.startTransition(() => setCount(count + 1))}
>
inc
</button>
<React.Suspense fallback={'Loading...'}>
<Page count={count} />
</React.Suspense>
</div>
)
}
function Page({ count }: { count: number }) {
const { data } = useSuspenseQuery({
queryKey: [key, count],
queryFn: async () => {
queryFnCount++
await sleep(10)
return 'data' + count
},
})
return (
<div>
<div>{String(data)}</div>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<App />,
)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('data0'))
fireEvent.click(rendered.getByText('inc'))
await waitFor(() => rendered.getByText('data1'))
await sleep(20)
expect(queryFnCount).toBe(2)
})
it('should still suspense if queryClient has placeholderData config', async () => {
const key = queryKey()
const queryClientWithPlaceholder = createQueryClient({
defaultOptions: {
queries: {
placeholderData: (previousData: any) => previousData,
},
},
})
function Page() {
const [count, setCount] = React.useState(0)
const [isPending, startTransition] = React.useTransition()
const { data } = useSuspenseQuery({
queryKey: [key, count],
queryFn: async () => {
await sleep(10)
return 'data' + count
},
})
return (
<div>
<button onClick={() => startTransition(() => setCount(count + 1))}>
inc
</button>
<div>{isPending ? 'Pending...' : String(data)}</div>
</div>
)
}
const rendered = renderWithClient(
queryClientWithPlaceholder,
<React.Suspense fallback={'Loading...'}>
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('data0'))
fireEvent.click(rendered.getByText('inc'))
await waitFor(() => rendered.getByText('Pending...'))
await waitFor(() => rendered.getByText('data1'))
})
it('should show error boundary even with gcTime:0 (#7853)', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let count = 0
function Page() {
useSuspenseQuery({
queryKey: key,
queryFn: async () => {
count++
console.log('queryFn')
throw new Error('Query failed')
},
gcTime: 0,
retry: false,
})
return null
}
function App() {
return (
<React.Suspense fallback="loading">
<ErrorBoundary
fallbackRender={() => {
return <div>There was an error!</div>
}}
>
<Page />
</ErrorBoundary>
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('There was an error!'))
expect(count).toBe(1)
consoleMock.mockRestore()
})
describe('gc (with fake timers)', () => {
beforeAll(() => {
vi.useFakeTimers()
})
afterAll(() => {
vi.useRealTimers()
})
it('should gc when unmounted while fetching with low gcTime (#8159)', async () => {
const key = queryKey()
function Page() {
return (
<React.Suspense fallback="loading">
<Component />
</React.Suspense>
)
}
function Component() {
const { data } = useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(3000)
return 'data'
},
gcTime: 1000,
})
return <div>{data}</div>
}
function Page2() {
return <div>page2</div>
}
function App() {
const [show, setShow] = React.useState(true)
return (
<div>
{show ? <Page /> : <Page2 />}
<button onClick={() => setShow(false)}>hide</button>
</div>
)
}
const rendered = renderWithClient(queryClient, <App />)
await act(() => vi.advanceTimersByTimeAsync(200))
rendered.getByText('loading')
// unmount while still fetching
fireEvent.click(rendered.getByText('hide'))
await act(() => vi.advanceTimersByTimeAsync(800))
rendered.getByText('page2')
// wait for query to be resolved
await act(() => vi.advanceTimersByTimeAsync(2000))
expect(queryClient.getQueryData(key)).toBe('data')
// wait for gc
await act(() => vi.advanceTimersByTimeAsync(1000))
expect(queryClient.getQueryData(key)).toBe(undefined)
})
})
})

View File

@@ -0,0 +1,71 @@
import { describe, expectTypeOf, it } from 'vitest'
import { skipToken } from '@tanstack/query-core'
import { useSuspenseQuery } from '../useSuspenseQuery'
describe('useSuspenseQuery', () => {
it('should always have data defined', () => {
const { data } = useSuspenseQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should not have pending status', () => {
const { status } = useSuspenseQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
expectTypeOf(status).toEqualTypeOf<'error' | 'success'>()
})
it('should not allow skipToken in queryFn', () => {
useSuspenseQuery({
queryKey: ['key'],
// @ts-expect-error
queryFn: skipToken,
})
useSuspenseQuery({
queryKey: ['key'],
// @ts-expect-error
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
})
})
it('should not allow placeholderData, enabled or throwOnError props', () => {
useSuspenseQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
placeholderData: 5,
enabled: true,
})
useSuspenseQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
enabled: true,
})
useSuspenseQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
throwOnError: true,
})
})
it('should not return isPlaceholderData', () => {
const query = useSuspenseQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
// @ts-expect-error TS2339
query.isPlaceholderData
})
})

View File

@@ -0,0 +1,873 @@
import { describe, expect, it, vi } from 'vitest'
import { fireEvent, waitFor } from '@testing-library/react'
import * as React from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import {
QueryCache,
QueryErrorResetBoundary,
useQueryErrorResetBoundary,
useSuspenseInfiniteQuery,
useSuspenseQuery,
} from '..'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
import type {
InfiniteData,
UseSuspenseInfiniteQueryResult,
UseSuspenseQueryResult,
} from '..'
describe('useSuspenseQuery', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })
it('should render the correct amount of times in Suspense mode', async () => {
const key = queryKey()
const states: Array<UseSuspenseQueryResult<number>> = []
let count = 0
let renders = 0
function Page() {
renders++
const [stateKey, setStateKey] = React.useState(key)
const state = useSuspenseQuery({
queryKey: stateKey,
queryFn: async () => {
count++
await sleep(10)
return count
},
})
states.push(state)
return (
<div>
<button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
data: {String(state.data)}
</div>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading">
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('data: 1'))
fireEvent.click(rendered.getByLabelText('toggle'))
await waitFor(() => rendered.getByText('data: 2'))
expect(renders).toBe(4)
expect(states.length).toBe(2)
expect(states[0]).toMatchObject({ data: 1, status: 'success' })
expect(states[1]).toMatchObject({ data: 2, status: 'success' })
})
it('should return the correct states for a successful infinite query', async () => {
const key = queryKey()
const states: Array<UseSuspenseInfiniteQueryResult<InfiniteData<number>>> =
[]
function Page() {
const [multiplier, setMultiplier] = React.useState(1)
const state = useSuspenseInfiniteQuery({
queryKey: [`${key}_${multiplier}`],
queryFn: async ({ pageParam }) => {
await sleep(10)
return Number(pageParam * multiplier)
},
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage + 1,
})
states.push(state)
return (
<div>
<button onClick={() => setMultiplier(2)}>next</button>
data: {state.data?.pages.join(',')}
</div>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading">
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('data: 1'))
expect(states.length).toBe(1)
expect(states[0]).toMatchObject({
data: { pages: [1], pageParams: [1] },
status: 'success',
})
fireEvent.click(rendered.getByText('next'))
await waitFor(() => rendered.getByText('data: 2'))
expect(states.length).toBe(2)
expect(states[1]).toMatchObject({
data: { pages: [2], pageParams: [1] },
status: 'success',
})
})
it('should not call the queryFn twice when used in Suspense mode', async () => {
const key = queryKey()
const queryFn = vi.fn<(...args: Array<unknown>) => string>()
queryFn.mockImplementation(() => {
sleep(10)
return 'data'
})
function Page() {
useSuspenseQuery({ queryKey: [key], queryFn })
return <>rendered</>
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading">
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('rendered'))
expect(queryFn).toHaveBeenCalledTimes(1)
})
it('should remove query instance when component unmounted', async () => {
const key = queryKey()
function Page() {
useSuspenseQuery({
queryKey: key,
queryFn: () => {
sleep(10)
return 'data'
},
})
return <>rendered</>
}
function App() {
const [show, setShow] = React.useState(false)
return (
<>
<React.Suspense fallback="loading">{show && <Page />}</React.Suspense>
<button
aria-label="toggle"
onClick={() => setShow((prev) => !prev)}
/>
</>
)
}
const rendered = renderWithClient(queryClient, <App />)
expect(rendered.queryByText('rendered')).toBeNull()
expect(queryCache.find({ queryKey: key })).toBeFalsy()
fireEvent.click(rendered.getByLabelText('toggle'))
await waitFor(() => rendered.getByText('rendered'))
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(1)
fireEvent.click(rendered.getByLabelText('toggle'))
expect(rendered.queryByText('rendered')).toBeNull()
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(0)
})
// https://github.com/tannerlinsley/react-query/issues/468
it('should reset error state if new component instances are mounted', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retryDelay: 10,
})
return <div>rendered</div>
}
const rendered = renderWithClient(
queryClient,
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
succeed = true
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<React.Suspense fallback={'Loading...'}>
<Page />
</React.Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>,
)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('rendered'))
expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual(
new Error('Suspense Error Bingo'),
)
consoleMock.mockRestore()
})
it('should retry fetch if the reset error boundary has been reset', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retry: false,
})
return <div>rendered</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>,
)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('rendered'))
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
function Component() {
const result = useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(5)
count++
return count
},
staleTime: () => 60 * 1000,
})
return (
<div>
<span>data: {result.data}</span>
</div>
)
}
function Page() {
return (
<React.Suspense fallback="Loading...">
<Component />
</React.Suspense>
)
}
const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('data: 1'))
expect(
typeof queryClient.getQueryCache().find({ queryKey: key })?.observers[0]
?.options.staleTime,
).toBe('function')
})
it('should suspend when switching to a new query', async () => {
const key1 = queryKey()
const key2 = queryKey()
function Component(props: { queryKey: Array<string> }) {
const result = useSuspenseQuery({
queryKey: props.queryKey,
queryFn: async () => {
await sleep(100)
return props.queryKey
},
retry: false,
})
return <div>data: {result.data}</div>
}
function Page() {
const [key, setKey] = React.useState(key1)
return (
<div>
<button
onClick={() => {
setKey(key2)
}}
>
switch
</button>
<React.Suspense fallback="Loading...">
<Component queryKey={key} />
</React.Suspense>
</div>
)
}
const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText(`data: ${key1}`))
fireEvent.click(rendered.getByText('switch'))
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText(`data: ${key2}`))
})
it('should retry fetch if the reset error boundary has been reset with global hook', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = false
function Page() {
useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retry: false,
})
return <div>rendered</div>
}
function App() {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetErrorBoundary()
}}
>
retry
</button>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('error boundary'))
await waitFor(() => rendered.getByText('retry'))
succeed = true
fireEvent.click(rendered.getByText('retry'))
await waitFor(() => rendered.getByText('rendered'))
consoleMock.mockRestore()
})
it('should throw errors to the error boundary by default', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
function Page() {
useSuspenseQuery({
queryKey: key,
queryFn: async (): Promise<unknown> => {
await sleep(10)
throw new Error('Suspense Error a1x')
},
retry: false,
})
return <div>rendered</div>
}
function App() {
return (
<ErrorBoundary
fallbackRender={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, <App />)
await waitFor(() => rendered.getByText('Loading...'))
await waitFor(() => rendered.getByText('error boundary'))
consoleMock.mockRestore()
})
it('should error caught in error boundary without infinite loop', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
const key = queryKey()
let succeed = true
function Page() {
const [nonce] = React.useState(0)
const queryKeys = [`${key}-${succeed}`]
const result = useSuspenseQuery({
queryKey: queryKeys,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return nonce
}
},
retry: false,
})
return (
<div>
<span>rendered</span> <span>{result.data}</span>
<button
aria-label="fail"
onClick={async () => {
await queryClient.resetQueries()
}}
>
fail
</button>
</div>
)
}
function App() {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={() => <div>error boundary</div>}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, <App />)
// render suspense fallback (Loading...)
await waitFor(() => rendered.getByText('Loading...'))
// resolve promise -> render Page (rendered)
await waitFor(() => rendered.getByText('rendered'))
// change query key
succeed = false
// reset query -> and throw error
fireEvent.click(rendered.getByLabelText('fail'))
// render error boundary fallback (error boundary)
await waitFor(() => rendered.getByText('error boundary'))
expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual(
new Error('Suspense Error Bingo'),
)
consoleMock.mockRestore()
})
it('should error caught in error boundary without infinite loop when query keys changed', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
let succeed = true
function Page() {
const [key, rerender] = React.useReducer((x) => x + 1, 0)
const queryKeys = [key, succeed]
const result = useSuspenseQuery({
queryKey: queryKeys,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retry: false,
})
if (result.error) {
throw result.error
}
return (
<div>
<span>rendered</span> <span>{result.data}</span>
<button aria-label="fail" onClick={rerender}>
fail
</button>
</div>
)
}
function App() {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={() => <div>error boundary</div>}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, <App />)
// render suspense fallback (Loading...)
await waitFor(() => rendered.getByText('Loading...'))
// resolve promise -> render Page (rendered)
await waitFor(() => rendered.getByText('rendered'))
// change promise result to error
succeed = false
// change query key
fireEvent.click(rendered.getByLabelText('fail'))
// render error boundary fallback (error boundary)
await waitFor(() => rendered.getByText('error boundary'))
expect(consoleMock.mock.calls[0]?.[1]).toStrictEqual(
new Error('Suspense Error Bingo'),
)
consoleMock.mockRestore()
})
it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => {
const key = queryKey()
let state: UseSuspenseQueryResult<number> | null = null
let count = 0
let renders = 0
function Page() {
renders++
state = useSuspenseQuery({
queryKey: key,
queryFn: async () => {
count++
await sleep(10)
return count
},
gcTime: 0,
})
return (
<div>
<span>rendered</span>
</div>
)
}
const rendered = renderWithClient(
queryClient,
<React.Suspense fallback="loading">
<Page />
</React.Suspense>,
)
await waitFor(() =>
expect(state).toMatchObject({
data: 1,
status: 'success',
}),
)
expect(renders).toBe(2)
await waitFor(() => expect(rendered.queryByText('rendered')).not.toBeNull())
})
it('should not throw background errors to the error boundary', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
let succeed = true
const key = queryKey()
function Page() {
const result = useSuspenseQuery({
queryKey: key,
queryFn: async () => {
await sleep(10)
if (!succeed) {
throw new Error('Suspense Error Bingo')
} else {
return 'data'
}
},
retry: false,
})
return (
<div>
<span>
rendered {result.data} {result.status}
</span>
<button onClick={() => result.refetch()}>refetch</button>
</div>
)
}
function App() {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={() => <div>error boundary</div>}
>
<React.Suspense fallback="Loading...">
<Page />
</React.Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, <App />)
// render suspense fallback (Loading...)
await waitFor(() => rendered.getByText('Loading...'))
// resolve promise -> render Page (rendered)
await waitFor(() => rendered.getByText('rendered data success'))
// change promise result to error
succeed = false
// refetch
fireEvent.click(rendered.getByRole('button', { name: 'refetch' }))
// we are now in error state but still have data to show
await waitFor(() => rendered.getByText('rendered data error'))
consoleMock.mockRestore()
})
it('should still suspense if queryClient has placeholderData config', async () => {
const key = queryKey()
const queryClientWithPlaceholder = createQueryClient({
defaultOptions: {
queries: {
placeholderData: (previousData: any) => previousData,
},
},
})
const states: Array<UseSuspenseQueryResult<number>> = []
let count = 0
let renders = 0
function Page() {
renders++
const [stateKey, setStateKey] = React.useState(key)
const state = useSuspenseQuery({
queryKey: stateKey,
queryFn: async () => {
count++
await sleep(100)
return count
},
})
states.push(state)
return (
<div>
<button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
data: {String(state.data)}
</div>
)
}
const rendered = renderWithClient(
queryClientWithPlaceholder,
<React.Suspense fallback="loading">
<Page />
</React.Suspense>,
)
await waitFor(() => rendered.getByText('loading'))
await waitFor(() => rendered.getByText('data: 1'))
fireEvent.click(rendered.getByLabelText('toggle'))
await waitFor(() => rendered.getByText('loading'))
await waitFor(() => rendered.getByText('data: 2'))
})
})

View File

@@ -0,0 +1,96 @@
import { vi } from 'vitest'
import * as React from 'react'
import { act, render } from '@testing-library/react'
import * as utils from '@tanstack/query-core'
import { QueryClient, QueryClientProvider, onlineManager } from '..'
import type { QueryClientConfig } from '..'
import type { MockInstance } from 'vitest'
export function renderWithClient(
client: QueryClient,
ui: React.ReactElement,
): ReturnType<typeof render> {
const { rerender, ...result } = render(
<QueryClientProvider client={client}>{ui}</QueryClientProvider>,
)
return {
...result,
rerender: (rerenderUi: React.ReactElement) =>
rerender(
<QueryClientProvider client={client}>{rerenderUi}</QueryClientProvider>,
),
} as any
}
export function Blink({
duration,
children,
}: {
duration: number
children: React.ReactNode
}) {
const [shouldShow, setShouldShow] = React.useState<boolean>(true)
React.useEffect(() => {
setShouldShow(true)
const timeout = setActTimeout(() => setShouldShow(false), duration)
return () => {
clearTimeout(timeout)
}
}, [duration, children])
return shouldShow ? <>{children}</> : <>off</>
}
export function createQueryClient(config?: QueryClientConfig): QueryClient {
return new QueryClient(config)
}
export function mockVisibilityState(
value: DocumentVisibilityState,
): MockInstance<() => DocumentVisibilityState> {
return vi.spyOn(document, 'visibilityState', 'get').mockReturnValue(value)
}
export function mockOnlineManagerIsOnline(
value: boolean,
): MockInstance<() => boolean> {
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
}
let queryKeyCount = 0
export function queryKey(): Array<string> {
queryKeyCount++
return [`query_${queryKeyCount}`]
}
export function sleep(timeout: number): Promise<void> {
return new Promise((resolve, _reject) => {
setTimeout(resolve, timeout)
})
}
export function setActTimeout(fn: () => void, ms?: number) {
return setTimeout(() => {
act(() => {
fn()
})
}, ms)
}
// This monkey-patches the isServer-value from utils,
// so that we can pretend to be in a server environment
export function setIsServer(isServer: boolean) {
const original = utils.isServer
Object.defineProperty(utils, 'isServer', {
get: () => isServer,
})
return () => {
Object.defineProperty(utils, 'isServer', {
get: () => original,
})
}
}
export const doNotExecute = (_func: () => void) => true

View File

@@ -0,0 +1,69 @@
'use client'
import * as React from 'react'
import { shouldThrowError } from './utils'
import type {
DefaultedQueryObserverOptions,
Query,
QueryKey,
QueryObserverResult,
ThrowOnError,
} from '@tanstack/query-core'
import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
export const ensurePreventErrorBoundaryRetry = <
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey extends QueryKey,
>(
options: DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
errorResetBoundary: QueryErrorResetBoundaryValue,
) => {
if (options.suspense || options.throwOnError) {
// Prevent retrying failed query if the error boundary has not been reset yet
if (!errorResetBoundary.isReset()) {
options.retryOnMount = false
}
}
}
export const useClearResetErrorBoundary = (
errorResetBoundary: QueryErrorResetBoundaryValue,
) => {
React.useEffect(() => {
errorResetBoundary.clearReset()
}, [errorResetBoundary])
}
export const getHasError = <
TData,
TError,
TQueryFnData,
TQueryData,
TQueryKey extends QueryKey,
>({
result,
errorResetBoundary,
throwOnError,
query,
}: {
result: QueryObserverResult<TData, TError>
errorResetBoundary: QueryErrorResetBoundaryValue
throwOnError: ThrowOnError<TQueryFnData, TError, TQueryData, TQueryKey>
query: Query<TQueryFnData, TError, TQueryData, TQueryKey> | undefined
}) => {
return (
result.isError &&
!errorResetBoundary.isReset() &&
!result.isFetching &&
query &&
shouldThrowError(throwOnError, [result.error, query])
)
}

55
node_modules/@tanstack/react-query/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,55 @@
/* istanbul ignore file */
// Re-export core
export * from '@tanstack/query-core'
// React Query
export * from './types'
export { useQueries } from './useQueries'
export type { QueriesResults, QueriesOptions } from './useQueries'
export { useQuery } from './useQuery'
export { useSuspenseQuery } from './useSuspenseQuery'
export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery'
export { useSuspenseQueries } from './useSuspenseQueries'
export type {
SuspenseQueriesResults,
SuspenseQueriesOptions,
} from './useSuspenseQueries'
export { usePrefetchQuery } from './usePrefetchQuery'
export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery'
export { queryOptions } from './queryOptions'
export type {
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
UnusedSkipTokenOptions,
} from './queryOptions'
export { infiniteQueryOptions } from './infiniteQueryOptions'
export type {
DefinedInitialDataInfiniteOptions,
UndefinedInitialDataInfiniteOptions,
UnusedSkipTokenInfiniteOptions,
} from './infiniteQueryOptions'
export {
QueryClientContext,
QueryClientProvider,
useQueryClient,
} from './QueryClientProvider'
export type { QueryClientProviderProps } from './QueryClientProvider'
export type { QueryErrorResetBoundaryProps } from './QueryErrorResetBoundary'
export { HydrationBoundary } from './HydrationBoundary'
export type { HydrationBoundaryProps } from './HydrationBoundary'
export type {
QueryErrorClearResetFunction,
QueryErrorIsResetFunction,
QueryErrorResetBoundaryFunction,
QueryErrorResetFunction,
} from './QueryErrorResetBoundary'
export {
QueryErrorResetBoundary,
useQueryErrorResetBoundary,
} from './QueryErrorResetBoundary'
export { useIsFetching } from './useIsFetching'
export { useIsMutating, useMutationState } from './useMutationState'
export { useMutation } from './useMutation'
export { useInfiniteQuery } from './useInfiniteQuery'
export { useIsRestoring, IsRestoringProvider } from './isRestoring'

View File

@@ -0,0 +1,160 @@
import type {
DataTag,
DefaultError,
InfiniteData,
InitialDataFunction,
OmitKeyof,
QueryKey,
SkipToken,
} from '@tanstack/query-core'
import type { UseInfiniteQueryOptions } from './types'
export type UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
> & {
initialData?:
| undefined
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| InitialDataFunction<
NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
>
}
export type UnusedSkipTokenInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = OmitKeyof<
UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>,
'queryFn'
> & {
queryFn?: Exclude<
UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>['queryFn'],
SkipToken | undefined
>
}
type NonUndefinedGuard<T> = T extends undefined ? never : T
export type DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
> & {
initialData:
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
| undefined
}
export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>>
}
export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UnusedSkipTokenInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): UnusedSkipTokenInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>>
}
export function infiniteQueryOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>>
}
export function infiniteQueryOptions(options: unknown) {
return options
}

View File

@@ -0,0 +1,7 @@
'use client'
import * as React from 'react'
const IsRestoringContext = React.createContext(false)
export const useIsRestoring = () => React.useContext(IsRestoringContext)
export const IsRestoringProvider = IsRestoringContext.Provider

86
node_modules/@tanstack/react-query/src/queryOptions.ts generated vendored Normal file
View File

@@ -0,0 +1,86 @@
import type {
DataTag,
DefaultError,
InitialDataFunction,
OmitKeyof,
QueryKey,
SkipToken,
} from '@tanstack/query-core'
import type { UseQueryOptions } from './types'
export type UndefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
initialData?:
| undefined
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
| NonUndefinedGuard<TQueryFnData>
}
export type UnusedSkipTokenOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryFn'
> & {
queryFn?: Exclude<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'],
SkipToken | undefined
>
}
type NonUndefinedGuard<T> = T extends undefined ? never : T
export type DefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
initialData:
| NonUndefinedGuard<TQueryFnData>
| (() => NonUndefinedGuard<TQueryFnData>)
}
export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData>
}
export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey>,
): UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData>
}
export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData>
}
export function queryOptions(options: unknown) {
return options
}

67
node_modules/@tanstack/react-query/src/suspense.ts generated vendored Normal file
View File

@@ -0,0 +1,67 @@
import type {
DefaultError,
DefaultedQueryObserverOptions,
Query,
QueryKey,
QueryObserver,
QueryObserverResult,
} from '@tanstack/query-core'
import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
export const defaultThrowOnError = <
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
_error: TError,
query: Query<TQueryFnData, TError, TData, TQueryKey>,
) => query.state.data === undefined
export const ensureSuspenseTimers = (
defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
) => {
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
}
if (typeof defaultedOptions.gcTime === 'number') {
defaultedOptions.gcTime = Math.max(defaultedOptions.gcTime, 1000)
}
}
}
export const willFetch = (
result: QueryObserverResult<any, any>,
isRestoring: boolean,
) => result.isLoading && result.isFetching && !isRestoring
export const shouldSuspend = (
defaultedOptions:
| DefaultedQueryObserverOptions<any, any, any, any, any>
| undefined,
result: QueryObserverResult<any, any>,
) => defaultedOptions?.suspense && result.isPending
export const fetchOptimistic = <
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey extends QueryKey,
>(
defaultedOptions: DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
errorResetBoundary: QueryErrorResetBoundaryValue,
) =>
observer.fetchOptimistic(defaultedOptions).catch(() => {
errorResetBoundary.clearReset()
})

191
node_modules/@tanstack/react-query/src/types.ts generated vendored Normal file
View File

@@ -0,0 +1,191 @@
/* istanbul ignore file */
import type {
DefaultError,
DefinedInfiniteQueryObserverResult,
DefinedQueryObserverResult,
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
MutateFunction,
MutationObserverOptions,
MutationObserverResult,
OmitKeyof,
Override,
QueryKey,
QueryObserverOptions,
QueryObserverResult,
SkipToken,
} from '@tanstack/query-core'
export interface UseBaseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
> {}
export interface UseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends OmitKeyof<
UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
'suspense'
> {}
export interface UseSuspenseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData'
> {
queryFn?: Exclude<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'],
SkipToken
>
}
export interface UseInfiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends OmitKeyof<
InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
'suspense'
> {}
export interface UseSuspenseInfiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends OmitKeyof<
UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData'
> {
queryFn?: Exclude<
UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>['queryFn'],
SkipToken
>
}
export type UseBaseQueryResult<
TData = unknown,
TError = DefaultError,
> = QueryObserverResult<TData, TError>
export type UseQueryResult<
TData = unknown,
TError = DefaultError,
> = UseBaseQueryResult<TData, TError>
export type UseSuspenseQueryResult<
TData = unknown,
TError = DefaultError,
> = OmitKeyof<
DefinedQueryObserverResult<TData, TError>,
'isPlaceholderData' | 'promise'
>
export type DefinedUseQueryResult<
TData = unknown,
TError = DefaultError,
> = DefinedQueryObserverResult<TData, TError>
export type UseInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
> = InfiniteQueryObserverResult<TData, TError>
export type DefinedUseInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
> = DefinedInfiniteQueryObserverResult<TData, TError>
export type UseSuspenseInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
> = OmitKeyof<
DefinedInfiniteQueryObserverResult<TData, TError>,
'isPlaceholderData' | 'promise'
>
export interface UseMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> extends OmitKeyof<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'_defaulted'
> {}
export type UseMutateFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> = (
...args: Parameters<MutateFunction<TData, TError, TVariables, TContext>>
) => void
export type UseMutateAsyncFunction<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
> = MutateFunction<TData, TError, TVariables, TContext>
export type UseBaseMutationResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
> = Override<
MutationObserverResult<TData, TError, TVariables, TContext>,
{ mutate: UseMutateFunction<TData, TError, TVariables, TContext> }
> & { mutateAsync: UseMutateAsyncFunction<TData, TError, TVariables, TContext> }
export type UseMutationResult<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
> = UseBaseMutationResult<TData, TError, TVariables, TContext>

162
node_modules/@tanstack/react-query/src/useBaseQuery.ts generated vendored Normal file
View File

@@ -0,0 +1,162 @@
'use client'
import * as React from 'react'
import { isServer, notifyManager } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import {
ensurePreventErrorBoundaryRetry,
getHasError,
useClearResetErrorBoundary,
} from './errorBoundaryUtils'
import { useIsRestoring } from './isRestoring'
import {
ensureSuspenseTimers,
fetchOptimistic,
shouldSuspend,
willFetch,
} from './suspense'
import { noop } from './utils'
import type {
QueryClient,
QueryKey,
QueryObserver,
QueryObserverResult,
} from '@tanstack/query-core'
import type { UseBaseQueryOptions } from './types'
export function useBaseQuery<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey extends QueryKey,
>(
options: UseBaseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
Observer: typeof QueryObserver,
queryClient?: QueryClient,
): QueryObserverResult<TData, TError> {
if (process.env.NODE_ENV !== 'production') {
if (typeof options !== 'object' || Array.isArray(options)) {
throw new Error(
'Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. Please use the error stack to find the culprit call. More info here: https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#supports-a-single-signature-one-object',
)
}
}
const client = useQueryClient(queryClient)
const isRestoring = useIsRestoring()
const errorResetBoundary = useQueryErrorResetBoundary()
const defaultedOptions = client.defaultQueryOptions(options)
;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.(
defaultedOptions,
)
// Make sure results are optimistically set in fetching state before subscribing or updating options
defaultedOptions._optimisticResults = isRestoring
? 'isRestoring'
: 'optimistic'
ensureSuspenseTimers(defaultedOptions)
ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary)
useClearResetErrorBoundary(errorResetBoundary)
// this needs to be invoked before creating the Observer because that can create a cache entry
const isNewCacheEntry = !client
.getQueryCache()
.get(defaultedOptions.queryHash)
const [observer] = React.useState(
() =>
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
client,
defaultedOptions,
),
)
const result = observer.getOptimisticResult(defaultedOptions)
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => {
const unsubscribe = isRestoring
? () => undefined
: observer.subscribe(notifyManager.batchCalls(onStoreChange))
// Update result to make sure we did not miss any query updates
// between creating the observer and subscribing to it.
observer.updateResult()
return unsubscribe
},
[observer, isRestoring],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
)
React.useEffect(() => {
// Do not notify on updates because of changes in the options because
// these changes should already be reflected in the optimistic result.
observer.setOptions(defaultedOptions, { listeners: false })
}, [defaultedOptions, observer])
// Handle suspense
if (shouldSuspend(defaultedOptions, result)) {
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
}
// Handle error boundary
if (
getHasError({
result,
errorResetBoundary,
throwOnError: defaultedOptions.throwOnError,
query: client
.getQueryCache()
.get<
TQueryFnData,
TError,
TQueryData,
TQueryKey
>(defaultedOptions.queryHash),
})
) {
throw result.error
}
;(client.getDefaultOptions().queries as any)?._experimental_afterQuery?.(
defaultedOptions,
result,
)
if (
defaultedOptions.experimental_prefetchInRender &&
!isServer &&
willFetch(result, isRestoring)
) {
const promise = isNewCacheEntry
? // Fetch immediately on render in order to ensure `.promise` is resolved even if the component is unmounted
fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
: // subscribe to the "cache promise" so that we can finalize the currentThenable once data comes in
client.getQueryCache().get(defaultedOptions.queryHash)?.promise
promise?.catch(noop).finally(() => {
// `.updateResult()` will trigger `.#currentThenable` to finalize
observer.updateResult()
})
}
// Handle result property usage tracking
return !defaultedOptions.notifyOnChangeProps
? observer.trackResult(result)
: result
}

View File

@@ -0,0 +1,82 @@
'use client'
import { InfiniteQueryObserver } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import type {
DefaultError,
InfiniteData,
QueryClient,
QueryKey,
QueryObserver,
} from '@tanstack/query-core'
import type {
DefinedUseInfiniteQueryResult,
UseInfiniteQueryOptions,
UseInfiniteQueryResult,
} from './types'
import type {
DefinedInitialDataInfiniteOptions,
UndefinedInitialDataInfiniteOptions,
} from './infiniteQueryOptions'
export function useInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
queryClient?: QueryClient,
): DefinedUseInfiniteQueryResult<TData, TError>
export function useInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
queryClient?: QueryClient,
): UseInfiniteQueryResult<TData, TError>
export function useInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>,
queryClient?: QueryClient,
): UseInfiniteQueryResult<TData, TError>
export function useInfiniteQuery(
options: UseInfiniteQueryOptions,
queryClient?: QueryClient,
) {
return useBaseQuery(
options,
InfiniteQueryObserver as typeof QueryObserver,
queryClient,
)
}

View File

@@ -0,0 +1,24 @@
'use client'
import * as React from 'react'
import { notifyManager } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import type { QueryClient, QueryFilters } from '@tanstack/query-core'
export function useIsFetching(
filters?: QueryFilters,
queryClient?: QueryClient,
): number {
const client = useQueryClient(queryClient)
const queryCache = client.getQueryCache()
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[queryCache],
),
() => client.isFetching(filters),
() => client.isFetching(filters),
)
}

65
node_modules/@tanstack/react-query/src/useMutation.ts generated vendored Normal file
View File

@@ -0,0 +1,65 @@
'use client'
import * as React from 'react'
import { MutationObserver, notifyManager } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import { noop, shouldThrowError } from './utils'
import type {
UseMutateFunction,
UseMutationOptions,
UseMutationResult,
} from './types'
import type { DefaultError, QueryClient } from '@tanstack/query-core'
// HOOK
export function useMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(
options: UseMutationOptions<TData, TError, TVariables, TContext>,
queryClient?: QueryClient,
): UseMutationResult<TData, TError, TVariables, TContext> {
const client = useQueryClient(queryClient)
const [observer] = React.useState(
() =>
new MutationObserver<TData, TError, TVariables, TContext>(
client,
options,
),
)
React.useEffect(() => {
observer.setOptions(options)
}, [observer, options])
const result = React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
observer.subscribe(notifyManager.batchCalls(onStoreChange)),
[observer],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
)
const mutate = React.useCallback<
UseMutateFunction<TData, TError, TVariables, TContext>
>(
(variables, mutateOptions) => {
observer.mutate(variables, mutateOptions).catch(noop)
},
[observer],
)
if (
result.error &&
shouldThrowError(observer.options.throwOnError, [result.error])
) {
throw result.error
}
return { ...result, mutate, mutateAsync: result.mutate }
}

View File

@@ -0,0 +1,77 @@
/* eslint-disable react-compiler/react-compiler */
'use client'
import * as React from 'react'
import { notifyManager, replaceEqualDeep } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import type {
Mutation,
MutationCache,
MutationFilters,
MutationState,
QueryClient,
} from '@tanstack/query-core'
export function useIsMutating(
filters?: MutationFilters,
queryClient?: QueryClient,
): number {
const client = useQueryClient(queryClient)
return useMutationState(
{ filters: { ...filters, status: 'pending' } },
client,
).length
}
type MutationStateOptions<TResult = MutationState> = {
filters?: MutationFilters
select?: (mutation: Mutation) => TResult
}
function getResult<TResult = MutationState>(
mutationCache: MutationCache,
options: MutationStateOptions<TResult>,
): Array<TResult> {
return mutationCache
.findAll(options.filters)
.map(
(mutation): TResult =>
(options.select ? options.select(mutation) : mutation.state) as TResult,
)
}
export function useMutationState<TResult = MutationState>(
options: MutationStateOptions<TResult> = {},
queryClient?: QueryClient,
): Array<TResult> {
const mutationCache = useQueryClient(queryClient).getMutationCache()
const optionsRef = React.useRef(options)
const result = React.useRef<Array<TResult>>(null)
if (!result.current) {
result.current = getResult(mutationCache, options)
}
React.useEffect(() => {
optionsRef.current = options
})
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
mutationCache.subscribe(() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
notifyManager.schedule(onStoreChange)
}
}),
[mutationCache],
),
() => result.current,
() => result.current,
)!
}

View File

@@ -0,0 +1,30 @@
import { useQueryClient } from './QueryClientProvider'
import type {
DefaultError,
FetchInfiniteQueryOptions,
QueryClient,
QueryKey,
} from '@tanstack/query-core'
export function usePrefetchInfiniteQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
queryClient?: QueryClient,
) {
const client = useQueryClient(queryClient)
if (!client.getQueryState(options.queryKey)) {
client.prefetchInfiniteQuery(options)
}
}

View File

@@ -0,0 +1,23 @@
import { useQueryClient } from './QueryClientProvider'
import type {
DefaultError,
FetchQueryOptions,
QueryClient,
QueryKey,
} from '@tanstack/query-core'
export function usePrefetchQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
queryClient?: QueryClient,
) {
const client = useQueryClient(queryClient)
if (!client.getQueryState(options.queryKey)) {
client.prefetchQuery(options)
}
}

346
node_modules/@tanstack/react-query/src/useQueries.ts generated vendored Normal file
View File

@@ -0,0 +1,346 @@
'use client'
import * as React from 'react'
import {
QueriesObserver,
QueryObserver,
notifyManager,
} from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import { useIsRestoring } from './isRestoring'
import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
import {
ensurePreventErrorBoundaryRetry,
getHasError,
useClearResetErrorBoundary,
} from './errorBoundaryUtils'
import {
ensureSuspenseTimers,
fetchOptimistic,
shouldSuspend,
willFetch,
} from './suspense'
import type {
DefinedUseQueryResult,
UseQueryOptions,
UseQueryResult,
} from './types'
import type {
DefaultError,
OmitKeyof,
QueriesObserverOptions,
QueriesPlaceholderDataFunction,
QueryClient,
QueryFunction,
QueryKey,
QueryObserverOptions,
ThrowOnError,
} from '@tanstack/query-core'
// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
// `placeholderData` function always gets undefined passed
type UseQueryOptionsForUseQueries<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'placeholderData'
> & {
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
}
// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
type SkipTokenForUseQueries = symbol
type GetUseQueryOptionsForUseQueries<T> =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseQueryOptionsForUseQueries<unknown, TError, TData>
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
: T extends [infer TQueryFnData, infer TError]
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseQueryOptionsForUseQueries<TQueryFnData>
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction<infer TQueryFnData, infer TQueryKey>
| SkipTokenForUseQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseQueryOptionsForUseQueries<
TQueryFnData,
unknown extends TError ? DefaultError : TError,
unknown extends TData ? TQueryFnData : TData,
TQueryKey
>
: // Fallback
UseQueryOptionsForUseQueries
// A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult
type GetDefinedOrUndefinedQueryResult<T, TData, TError = unknown> = T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
? UseQueryResult<TData, TError>
: TInitialData extends TData
? DefinedUseQueryResult<TData, TError>
: TInitialData extends () => infer TInitialDataResult
? unknown extends TInitialDataResult
? UseQueryResult<TData, TError>
: TInitialDataResult extends TData
? DefinedUseQueryResult<TData, TError>
: UseQueryResult<TData, TError>
: UseQueryResult<TData, TError>
: UseQueryResult<TData, TError>
type GetUseQueryResult<T> =
// Part 1: responsible for mapping explicit type parameter to function result, if object
T extends { queryFnData: any; error?: infer TError; data: infer TData }
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
T extends [any, infer TError, infer TData]
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: T extends [infer TQueryFnData, infer TError]
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
: T extends [infer TQueryFnData]
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction<infer TQueryFnData, any>
| SkipTokenForUseQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? GetDefinedOrUndefinedQueryResult<
T,
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: // Fallback
UseQueryResult
/**
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
*/
export type QueriesOptions<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseQueryOptionsForUseQueries>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseQueryOptionsForUseQueries<Head>]
: T extends [infer Head, ...infer Tails]
? QueriesOptions<
[...Tails],
[...TResults, GetUseQueryOptionsForUseQueries<Head>],
[...TDepth, 1]
>
: ReadonlyArray<unknown> extends T
? T
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
// use this to infer the param types in the case of Array.map() argument
T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
infer TQueryKey
>
>
? Array<
UseQueryOptionsForUseQueries<
TQueryFnData,
TError,
TData,
TQueryKey
>
>
: // Fallback
Array<UseQueryOptionsForUseQueries>
/**
* QueriesResults reducer recursively maps type param to results
*/
export type QueriesResults<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseQueryResult>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseQueryResult<Head>]
: T extends [infer Head, ...infer Tails]
? QueriesResults<
[...Tails],
[...TResults, GetUseQueryResult<Head>],
[...TDepth, 1]
>
: T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
Array<
UseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
>
: // Fallback
Array<UseQueryResult>
export function useQueries<
T extends Array<any>,
TCombinedResult = QueriesResults<T>,
>(
{
queries,
...options
}: {
queries: readonly [...QueriesOptions<T>]
combine?: (result: QueriesResults<T>) => TCombinedResult
},
queryClient?: QueryClient,
): TCombinedResult {
const client = useQueryClient(queryClient)
const isRestoring = useIsRestoring()
const errorResetBoundary = useQueryErrorResetBoundary()
const defaultedQueries = React.useMemo(
() =>
queries.map((opts) => {
const defaultedOptions = client.defaultQueryOptions(
opts as QueryObserverOptions,
)
// Make sure the results are already in fetching state before subscribing or updating options
defaultedOptions._optimisticResults = isRestoring
? 'isRestoring'
: 'optimistic'
return defaultedOptions
}),
[queries, client, isRestoring],
)
defaultedQueries.forEach((query) => {
ensureSuspenseTimers(query)
ensurePreventErrorBoundaryRetry(query, errorResetBoundary)
})
useClearResetErrorBoundary(errorResetBoundary)
const [observer] = React.useState(
() =>
new QueriesObserver<TCombinedResult>(
client,
defaultedQueries,
options as QueriesObserverOptions<TCombinedResult>,
),
)
const [optimisticResult, getCombinedResult, trackResult] =
observer.getOptimisticResult(
defaultedQueries,
(options as QueriesObserverOptions<TCombinedResult>).combine,
)
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
isRestoring
? () => undefined
: observer.subscribe(notifyManager.batchCalls(onStoreChange)),
[observer, isRestoring],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
)
React.useEffect(() => {
// Do not notify on updates because of changes in the options because
// these changes should already be reflected in the optimistic result.
observer.setQueries(
defaultedQueries,
options as QueriesObserverOptions<TCombinedResult>,
{
listeners: false,
},
)
}, [defaultedQueries, options, observer])
const shouldAtLeastOneSuspend = optimisticResult.some((result, index) =>
shouldSuspend(defaultedQueries[index], result),
)
const suspensePromises = shouldAtLeastOneSuspend
? optimisticResult.flatMap((result, index) => {
const opts = defaultedQueries[index]
if (opts) {
const queryObserver = new QueryObserver(client, opts)
if (shouldSuspend(opts, result)) {
return fetchOptimistic(opts, queryObserver, errorResetBoundary)
} else if (willFetch(result, isRestoring)) {
void fetchOptimistic(opts, queryObserver, errorResetBoundary)
}
}
return []
})
: []
if (suspensePromises.length > 0) {
throw Promise.all(suspensePromises)
}
const firstSingleResultWhichShouldThrow = optimisticResult.find(
(result, index) => {
const query = defaultedQueries[index]
return (
query &&
getHasError({
result,
errorResetBoundary,
throwOnError: query.throwOnError,
query: client.getQueryCache().get(query.queryHash),
})
)
},
)
if (firstSingleResultWhichShouldThrow?.error) {
throw firstSingleResultWhichShouldThrow.error
}
return getCombinedResult(trackResult())
}

47
node_modules/@tanstack/react-query/src/useQuery.ts generated vendored Normal file
View File

@@ -0,0 +1,47 @@
'use client'
import { QueryObserver } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
import type {
DefinedUseQueryResult,
UseQueryOptions,
UseQueryResult,
} from './types'
import type {
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
} from './queryOptions'
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError>
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
queryClient?: QueryClient,
): UseQueryResult<TData, TError>
export function useQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
queryClient?: QueryClient,
): UseQueryResult<TData, TError>
export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) {
return useBaseQuery(options, QueryObserver, queryClient)
}

View File

@@ -0,0 +1,51 @@
'use client'
import { InfiniteQueryObserver, skipToken } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import { defaultThrowOnError } from './suspense'
import type {
DefaultError,
InfiniteData,
InfiniteQueryObserverSuccessResult,
QueryClient,
QueryKey,
QueryObserver,
} from '@tanstack/query-core'
import type {
UseSuspenseInfiniteQueryOptions,
UseSuspenseInfiniteQueryResult,
} from './types'
export function useSuspenseInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UseSuspenseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>,
queryClient?: QueryClient,
): UseSuspenseInfiniteQueryResult<TData, TError> {
if (process.env.NODE_ENV !== 'production') {
if ((options.queryFn as any) === skipToken) {
console.error('skipToken is not allowed for useSuspenseInfiniteQuery')
}
}
return useBaseQuery(
{
...options,
enabled: true,
suspense: true,
throwOnError: defaultThrowOnError,
},
InfiniteQueryObserver as typeof QueryObserver,
queryClient,
) as InfiniteQueryObserverSuccessResult<TData, TError>
}

View File

@@ -0,0 +1,212 @@
'use client'
import { skipToken } from '@tanstack/query-core'
import { useQueries } from './useQueries'
import { defaultThrowOnError } from './suspense'
import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types'
import type {
DefaultError,
QueryClient,
QueryFunction,
ThrowOnError,
} from '@tanstack/query-core'
// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
type SkipTokenForUseQueries = symbol
type GetUseSuspenseQueryOptions<T> =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
? UseSuspenseQueryOptions<TQueryFnData, TError, TData>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseSuspenseQueryOptions<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseSuspenseQueryOptions<unknown, TError, TData>
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
? UseSuspenseQueryOptions<TQueryFnData, TError, TData>
: T extends [infer TQueryFnData, infer TError]
? UseSuspenseQueryOptions<TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseSuspenseQueryOptions<TQueryFnData>
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction<infer TQueryFnData, infer TQueryKey>
| SkipTokenForUseQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseSuspenseQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey
>
: T extends {
queryFn?:
| QueryFunction<infer TQueryFnData, infer TQueryKey>
| SkipTokenForUseQueries
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseSuspenseQueryOptions<
TQueryFnData,
TError,
TQueryFnData,
TQueryKey
>
: // Fallback
UseSuspenseQueryOptions
type GetUseSuspenseQueryResult<T> =
// Part 1: responsible for mapping explicit type parameter to function result, if object
T extends { queryFnData: any; error?: infer TError; data: infer TData }
? UseSuspenseQueryResult<TData, TError>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseSuspenseQueryResult<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseSuspenseQueryResult<TData, TError>
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
T extends [any, infer TError, infer TData]
? UseSuspenseQueryResult<TData, TError>
: T extends [infer TQueryFnData, infer TError]
? UseSuspenseQueryResult<TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseSuspenseQueryResult<TQueryFnData>
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
T extends {
queryFn?:
| QueryFunction<infer TQueryFnData, any>
| SkipTokenForUseQueries
select?: (data: any) => infer TData
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseSuspenseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: T extends {
queryFn?:
| QueryFunction<infer TQueryFnData, any>
| SkipTokenForUseQueries
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseSuspenseQueryResult<
TQueryFnData,
unknown extends TError ? DefaultError : TError
>
: // Fallback
UseSuspenseQueryResult
/**
* SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
*/
export type SuspenseQueriesOptions<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseSuspenseQueryOptions>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseSuspenseQueryOptions<Head>]
: T extends [infer Head, ...infer Tails]
? SuspenseQueriesOptions<
[...Tails],
[...TResults, GetUseSuspenseQueryOptions<Head>],
[...TDepth, 1]
>
: Array<unknown> extends T
? T
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
// use this to infer the param types in the case of Array.map() argument
T extends Array<
UseSuspenseQueryOptions<
infer TQueryFnData,
infer TError,
infer TData,
infer TQueryKey
>
>
? Array<
UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>
>
: // Fallback
Array<UseSuspenseQueryOptions>
/**
* SuspenseQueriesResults reducer recursively maps type param to results
*/
export type SuspenseQueriesResults<
T extends Array<any>,
TResults extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseSuspenseQueryResult>
: T extends []
? []
: T extends [infer Head]
? [...TResults, GetUseSuspenseQueryResult<Head>]
: T extends [infer Head, ...infer Tails]
? SuspenseQueriesResults<
[...Tails],
[...TResults, GetUseSuspenseQueryResult<Head>],
[...TDepth, 1]
>
: T extends Array<
UseSuspenseQueryOptions<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
Array<
UseSuspenseQueryResult<
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
>
: // Fallback
Array<UseSuspenseQueryResult>
export function useSuspenseQueries<
T extends Array<any>,
TCombinedResult = SuspenseQueriesResults<T>,
>(
options: {
queries: readonly [...SuspenseQueriesOptions<T>]
combine?: (result: SuspenseQueriesResults<T>) => TCombinedResult
},
queryClient?: QueryClient,
): TCombinedResult {
return useQueries(
{
...options,
queries: options.queries.map((query) => {
if (process.env.NODE_ENV !== 'production') {
if (query.queryFn === skipToken) {
console.error('skipToken is not allowed for useSuspenseQueries')
}
}
return {
...query,
suspense: true,
throwOnError: defaultThrowOnError,
enabled: true,
placeholderData: undefined,
}
}),
} as any,
queryClient,
)
}

View File

@@ -0,0 +1,34 @@
'use client'
import { QueryObserver, skipToken } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import { defaultThrowOnError } from './suspense'
import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types'
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
export function useSuspenseQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
queryClient?: QueryClient,
): UseSuspenseQueryResult<TData, TError> {
if (process.env.NODE_ENV !== 'production') {
if ((options.queryFn as any) === skipToken) {
console.error('skipToken is not allowed for useSuspenseQuery')
}
}
return useBaseQuery(
{
...options,
enabled: true,
suspense: true,
throwOnError: defaultThrowOnError,
placeholderData: undefined,
},
QueryObserver,
queryClient,
) as UseSuspenseQueryResult<TData, TError>
}

13
node_modules/@tanstack/react-query/src/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,13 @@
export function shouldThrowError<T extends (...args: Array<any>) => boolean>(
throwError: boolean | T | undefined,
params: Parameters<T>,
): boolean {
// Allow throwError function to override throwing behavior on a per-error basis
if (typeof throwError === 'function') {
return throwError(...params)
}
return !!throwError
}
export function noop() {}