diff --git a/.gitignore b/.gitignore
index 9ad1f8224b..93fed3635a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,7 +28,6 @@ size-plugin.json
stats-hydration.json
stats.json
stats.html
-.vscode/settings.json
*.log
*.tsbuildinfo
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..9bf4d12b52
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+}
diff --git a/examples/react/transition/.eslintrc b/examples/react/transition/.eslintrc
new file mode 100644
index 0000000000..4e03b9e10b
--- /dev/null
+++ b/examples/react/transition/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": ["plugin:react/jsx-runtime", "plugin:react-hooks/recommended"]
+}
diff --git a/examples/react/transition/.gitignore b/examples/react/transition/.gitignore
new file mode 100644
index 0000000000..4673b022e5
--- /dev/null
+++ b/examples/react/transition/.gitignore
@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+pnpm-lock.yaml
+yarn.lock
+package-lock.json
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/examples/react/transition/README.md b/examples/react/transition/README.md
new file mode 100644
index 0000000000..93f18812e1
--- /dev/null
+++ b/examples/react/transition/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `pnpm install`
+- `pnpm dev`
diff --git a/examples/react/transition/index.html b/examples/react/transition/index.html
new file mode 100644
index 0000000000..aca35c1a17
--- /dev/null
+++ b/examples/react/transition/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+ TanStack Query React Suspense Example App
+
+
+
+
+
+
+
diff --git a/examples/react/transition/package.json b/examples/react/transition/package.json
new file mode 100644
index 0000000000..17d5de31b4
--- /dev/null
+++ b/examples/react/transition/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@tanstack/query-example-react-transition",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.62.8",
+ "@tanstack/react-query-devtools": "^5.62.8",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.3",
+ "typescript": "5.7.2",
+ "vite": "^5.3.5"
+ }
+}
diff --git a/examples/react/transition/public/emblem-light.svg b/examples/react/transition/public/emblem-light.svg
new file mode 100644
index 0000000000..a58e69ad5e
--- /dev/null
+++ b/examples/react/transition/public/emblem-light.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/examples/react/transition/src/index.tsx b/examples/react/transition/src/index.tsx
new file mode 100755
index 0000000000..a8f03fee3b
--- /dev/null
+++ b/examples/react/transition/src/index.tsx
@@ -0,0 +1,102 @@
+import {
+ QueryClient,
+ QueryClientProvider,
+ useQuery,
+} from '@tanstack/react-query'
+import { Suspense, use, useState, useTransition } from 'react'
+import ReactDOM from 'react-dom/client'
+
+const Example1 = ({ value }: { value: number }) => {
+ const { isFetching, promise } = useQuery({
+ queryKey: ['1' + value],
+ queryFn: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ return '1' + value
+ },
+ })
+ const data = use(promise)
+
+ return (
+
+ {data} {isFetching ? 'fetching' : null}
+
+ )
+}
+
+const Example2 = ({ value }: { value: number }) => {
+ const { promise, isFetching } = useQuery({
+ queryKey: ['2' + value],
+ queryFn: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ return '2' + value
+ },
+ // placeholderData: keepPreviousData,
+ })
+
+ const data = use(promise)
+
+ return (
+
+ {data} {isFetching ? 'fetching' : null}
+
+ )
+}
+
+const SuspenseBoundary = () => {
+ const [state, setState] = useState(-1)
+ const [isPending, startTransition] = useTransition()
+
+ return (
+
+
Change state with transition
+
+
+
+
State:
+
+ - last state value: {state}
+ -
+ transition state: {isPending ? pending : 'idle'}
+
+
+
2. 1 Suspense + startTransition
+
+
+
+
2.2 Suspense + startTransition
+
+
+
+
+ )
+}
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ experimental_prefetchInRender: true,
+ staleTime: 10 * 1000,
+ },
+ },
+})
+
+const App = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+const rootElement = document.getElementById('root') as HTMLElement
+ReactDOM.createRoot(rootElement).render()
diff --git a/examples/react/transition/tsconfig.json b/examples/react/transition/tsconfig.json
new file mode 100644
index 0000000000..23a8707ef4
--- /dev/null
+++ b/examples/react/transition/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "eslint.config.js"]
+}
diff --git a/examples/react/transition/vite.config.ts b/examples/react/transition/vite.config.ts
new file mode 100644
index 0000000000..9ffcc67574
--- /dev/null
+++ b/examples/react/transition/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts
index 3dc751f5cd..e360f571ce 100644
--- a/packages/query-core/src/queryObserver.ts
+++ b/packages/query-core/src/queryObserver.ts
@@ -698,11 +698,11 @@ export class QueryObserver<
includedProps.add('error')
}
- return Object.keys(this.#currentResult).some((key) => {
+ return [...includedProps].some((key) => {
const typedKey = key as keyof QueryObserverResult
const changed = this.#currentResult[typedKey] !== prevResult[typedKey]
- return changed && includedProps.has(typedKey)
+ return changed
})
}
diff --git a/packages/react-query/src/__tests__/transition.test.tsx b/packages/react-query/src/__tests__/transition.test.tsx
new file mode 100644
index 0000000000..833b4bde41
--- /dev/null
+++ b/packages/react-query/src/__tests__/transition.test.tsx
@@ -0,0 +1,288 @@
+/* eslint-disable @typescript-eslint/require-await */
+import { act, render, screen } from '@testing-library/react'
+import * as React from 'react'
+import { afterAll, beforeAll, expect, it, vi } from 'vitest'
+import { QueryClientProvider, useQuery } from '..'
+import { QueryCache } from '../index'
+import { createQueryClient, queryKey, sleep } from './utils'
+
+const queryCache = new QueryCache()
+const queryClient = createQueryClient({
+ queryCache,
+})
+
+beforeAll(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ global.IS_REACT_ACT_ENVIRONMENT = true
+ queryClient.setDefaultOptions({
+ queries: { experimental_prefetchInRender: true },
+ })
+ vi.useFakeTimers()
+})
+afterAll(() => {
+ queryClient.setDefaultOptions({
+ queries: { experimental_prefetchInRender: false },
+ })
+ vi.useRealTimers()
+})
+
+it('should keep values of old key around with startTransition', async () => {
+ const key = queryKey()
+
+ function Loading() {
+ return <>loading...>
+ }
+
+ function Page() {
+ const [isPending, startTransition] = React.useTransition()
+ const [count, setCount] = React.useState(0)
+ const query = useQuery({
+ queryKey: [key, count],
+ queryFn: async () => {
+ await sleep(10)
+ return 'test' + count
+ },
+ staleTime: 1000,
+ })
+
+ const data = React.use(query.promise)
+
+ return (
+
+
+
data: {data}
+ {isPending &&
pending...}
+
+ )
+ }
+ // Initial render should show fallback
+ await act(async () => {
+ render(
+
+ }>
+
+
+ ,
+ )
+ })
+
+ screen.getByText('loading...')
+ expect(screen.queryByText('button')).toBeNull()
+ expect(screen.queryByText('pending...')).toBeNull()
+ expect(screen.queryByText('data: test0')).toBeNull()
+
+ // Resolve the query, should show the data
+ await act(async () => {
+ vi.runAllTimers()
+ })
+
+ expect(screen.queryByText('loading...')).toBeNull()
+ screen.getByRole('button')
+ expect(screen.queryByText('pending...')).toBeNull()
+ screen.getByText('data: test0')
+
+ // Update in a transition, should show pending state, and existing content
+ await act(async () => {
+ screen.getByRole('button', { name: 'increment' }).click()
+ })
+ expect(screen.queryByText('loading...')).toBeNull()
+ screen.getByRole('button')
+ screen.getByText('pending...')
+ screen.getByText('data: test0')
+
+ // Resolve the query, should show the new data and no pending state
+ await act(async () => {
+ vi.runAllTimers()
+ })
+ expect(screen.queryByText('loading...')).toBeNull()
+ screen.getByRole('button')
+ expect(screen.queryByText('pending...')).toBeNull()
+ screen.getByText('data: test1')
+})
+
+it('should handle parallel queries with shared parent key in transition', async () => {
+ function ComponentA(props: { parentId: number }) {
+ const query = useQuery({
+ queryKey: ['A', props.parentId],
+ queryFn: async () => {
+ await sleep(10)
+ return `A-${props.parentId}`
+ },
+ staleTime: 1000,
+ })
+
+ const data = React.use(query.promise)
+ return A data: {data}
+ }
+
+ function ComponentALoading() {
+ return A loading...
+ }
+
+ function ComponentB(props: { parentId: number }) {
+ const query = useQuery({
+ queryKey: ['B', props.parentId],
+ queryFn: async () => {
+ await sleep(10)
+ return `B-${props.parentId}`
+ },
+ staleTime: 1000,
+ })
+
+ const data = React.use(query.promise)
+ return B data: {data}
+ }
+
+ function ComponentBLoading() {
+ return B loading...
+ }
+
+ function Parent() {
+ const [count, setCount] = React.useState(0)
+ const [isPending, startTransition] = React.useTransition()
+ return (
+
+
+ }>
+
+
+ }>
+
+
+ {isPending && pending...}
+
+ )
+ }
+
+ // Initial render should show fallback
+ await act(async () => {
+ render(
+
+
+ ,
+ )
+ })
+
+ screen.getByText('A loading...')
+ screen.getByText('B loading...')
+
+ // Resolve the query, should show the data
+ await act(async () => {
+ vi.runAllTimers()
+ })
+
+ screen.getByText('A data: A-0')
+ screen.getByText('B data: B-0')
+
+ // Update in a transition, should show pending state, and existing content
+ await act(async () => {
+ screen.getByRole('button', { name: 'increment' }).click()
+ })
+
+ screen.getByText('pending...')
+ screen.getByText('A data: A-0')
+ screen.getByText('B data: B-0')
+
+ // Resolve the query, should show the new data and no pending state
+ await act(async () => {
+ vi.runAllTimers()
+ })
+ screen.getByText('A data: A-1')
+ screen.getByText('B data: B-1')
+ expect(screen.queryByText('pending...')).toBeNull()
+})
+
+it('should work to interrupt a transition', async () => {
+ const resolversByCount: Record void> = {}
+
+ const key = queryKey()
+
+ function Component(props: { count: number }) {
+ const { count } = props
+
+ const query = useQuery({
+ queryKey: [key, count],
+ queryFn: async () => {
+ await new Promise((resolve) => {
+ resolversByCount[count] = resolve
+ })
+
+ return 'test' + count
+ },
+ staleTime: 1000,
+ })
+ const data = React.use(query.promise)
+ return data: {data}
+ }
+
+ function Page() {
+ const [isPending, startTransition] = React.useTransition()
+ const [count, setCount] = React.useState(0)
+
+ return (
+
+
+
+
+
+ {isPending && 'pending...'}
+
+ )
+ }
+ // Initial render should show fallback
+ await act(async () => {
+ render(
+
+
+ ,
+ )
+ })
+
+ screen.getByText('loading...')
+ expect(screen.queryByText('button')).toBeNull()
+ expect(screen.queryByText('pending...')).toBeNull()
+ expect(screen.queryByText('data: test0')).toBeNull()
+
+ // Resolve the query, should show the data
+ await act(async () => {
+ resolversByCount[0]!()
+ })
+
+ screen.getByText('data: test0')
+
+ // increment
+ await act(async () => {
+ screen.getByRole('button', { name: 'increment' }).click()
+ })
+
+ // should show pending state, and existing content
+ screen.getByText('pending...')
+ screen.getByText('data: test0')
+
+ // Before the query is resolved, increment again
+ await act(async () => {
+ screen.getByRole('button', { name: 'increment' }).click()
+ })
+
+ await act(async () => {
+ // resolve the second query
+ resolversByCount[1]!()
+ })
+
+ screen.getByText('pending...')
+ screen.getByText('data: test0')
+
+ await act(async () => {
+ // resolve the third query
+ resolversByCount[2]!()
+ })
+
+ screen.getByText('data: test2')
+})
diff --git a/packages/react-query/src/__tests__/useQuery.promise.test.tsx b/packages/react-query/src/__tests__/useQuery.promise.test.tsx
index ec07d2d2b2..100e0fa1ce 100644
--- a/packages/react-query/src/__tests__/useQuery.promise.test.tsx
+++ b/packages/react-query/src/__tests__/useQuery.promise.test.tsx
@@ -1,10 +1,12 @@
-import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
-import * as React from 'react'
-import { ErrorBoundary } from 'react-error-boundary'
import {
createRenderStream,
+ disableActEnvironment,
useTrackRenders,
} from '@testing-library/react-render-stream'
+import * as React from 'react'
+import { ErrorBoundary } from 'react-error-boundary'
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
+import { act, fireEvent, waitFor } from '@testing-library/react'
import {
QueryClientProvider,
QueryErrorResetBoundary,
@@ -12,7 +14,17 @@ import {
useQuery,
} from '..'
import { QueryCache } from '../index'
-import { createQueryClient, queryKey, sleep } from './utils'
+import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
+
+function createDeferred() {
+ let resolve: (value: T) => void
+ let reject: (error: unknown) => void
+ const promise = new Promise((_resolve, _reject) => {
+ resolve = _resolve
+ reject = _reject
+ })
+ return { promise, resolve: resolve!, reject: reject! }
+}
describe('useQuery().promise', () => {
const queryCache = new QueryCache()
@@ -31,1350 +43,1453 @@ describe('useQuery().promise', () => {
})
})
- it('should work with a basic test', async () => {
- const key = queryKey()
+ describe('testing lib tests', () => {
+ it('should throw error if query fails with deferred value #8249', async () => {
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ const key = queryKey()
+ let renderCount = 0
+
+ function Page() {
+ renderCount++
+
+ const [_count, setCount] = React.useState(0)
+ const count = React.useDeferredValue(_count)
+
+ const query = useQuery({
+ queryKey: [key, count],
+ queryFn: async () => {
+ await sleep(10)
+ // succeed only on first query
+ if (count === 0) {
+ return 'test' + count
+ }
+ throw new Error('Error test')
+ },
+ retry: false,
+ })
+
+ return (
+
+
+
+
+ )
+ }
+
+ const rendered = await act(() =>
+ renderWithClient(
+ queryClient,
+ error boundary
}>
+
+ ,
+ ),
+ )
- const renderStream = createRenderStream({ snapshotDOM: true })
+ await waitFor(() => rendered.getByText('loading..'))
+ await waitFor(() => rendered.getByText('test0'))
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
- useTrackRenders()
- return <>{data}>
- }
+ const consoleMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => undefined)
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
+ await act(() => fireEvent.click(rendered.getByText('inc')))
- function Page() {
- useTrackRenders()
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- return 'test'
- },
- })
+ await waitFor(() => rendered.getByText('error boundary'))
- return (
- }>
-
-
- )
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
+ consoleMock.mockRestore()
+
+ expect(renderCount).toBe(6)
+ })
})
- it('colocate suspense and promise', async () => {
- const key = queryKey()
- let callCount = 0
+ describe('renderStream tests', () => {
+ let disableActReturn: ReturnType
+ beforeAll(() => {
+ disableActReturn = disableActEnvironment()
+ })
+ afterAll(() => {
+ disableActReturn.cleanup()
+ })
- const renderStream = createRenderStream({ snapshotDOM: true })
+ it('should work with a basic test', async () => {
+ const key = queryKey()
+
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+ useTrackRenders()
+ return <>{data}>
+ }
+
+ function Loading() {
+ useTrackRenders()
+ return <>loading..>
+ }
+
+ function Page() {
+ useTrackRenders()
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(1)
+ return 'test'
+ },
+ })
+
+ return (
+ }>
+
+
+ )
+ }
- function MyComponent() {
- useTrackRenders()
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- callCount++
- await sleep(1)
- return 'test'
- },
- staleTime: 1000,
- })
- const data = React.use(query.promise)
-
- return <>{data}>
- }
-
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
- function Page() {
- useTrackRenders()
- return (
- }>
-
-
+ await renderStream.render(
+
+
+ ,
)
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test')
- expect(renderedComponents).toEqual([MyComponent])
- }
-
- expect(callCount).toBe(1)
- })
- it('parallel queries', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
- let callCount = 0
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
+ })
- function MyComponent() {
- useTrackRenders()
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- callCount++
- await sleep(1)
- return 'test'
- },
- staleTime: 1000,
- })
- const data = React.use(query.promise)
-
- return data
- }
-
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
- function Page() {
- useTrackRenders()
- return (
- <>
+ it('colocate suspense and promise', async () => {
+ const key = queryKey()
+ let callCount = 0
+
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent() {
+ useTrackRenders()
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ callCount++
+ await sleep(1)
+ return 'test'
+ },
+ staleTime: 1000,
+ })
+ const data = React.use(query.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ useTrackRenders()
+ return <>loading..>
+ }
+ function Page() {
+ useTrackRenders()
+ return (
}>
-
-
-
-
-
-
- >
- )
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('testtesttesttesttest')
- expect(renderedComponents).toEqual([
- MyComponent,
- MyComponent,
- MyComponent,
- MyComponent,
- MyComponent,
- ])
- }
-
- expect(callCount).toBe(1)
- })
-
- it('should work with initial data', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
-
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
-
- return <>{data}>
- }
- function Loading() {
- useTrackRenders()
-
- return <>loading..>
- }
- function Page() {
- useTrackRenders()
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- return 'test'
- },
- initialData: 'initial',
- })
+ )
+ }
- return (
- }>
-
-
+ await renderStream.render(
+
+
+ ,
)
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('initial')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
- })
- it('should not fetch with initial data and staleTime', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
- const queryFn = vi.fn().mockImplementation(async () => {
- await sleep(1)
- return 'test'
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
+
+ expect(callCount).toBe(1)
})
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
-
- return <>{data}>
- }
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
- function Page() {
- useTrackRenders()
- const query = useQuery({
- queryKey: key,
- queryFn,
- initialData: 'initial',
- staleTime: 1000,
- })
-
- return (
- }>
-
-
+ it('parallel queries', async () => {
+ const deferred = createDeferred()
+ const key = queryKey()
+
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ let callCount = 0
+
+ function MyComponent() {
+ useTrackRenders()
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ callCount++
+ await deferred.promise
+ return 'test'
+ },
+ staleTime: 1000,
+ })
+ const data = React.use(query.promise)
+
+ return data
+ }
+
+ function Loading() {
+ useTrackRenders()
+ return loading..
+ }
+ function Page() {
+ useTrackRenders()
+ return (
+ <>
+ }>
+
+
+
+
+ loading 2...}>
+
+
+
+ >
+ )
+ }
+
+ await renderStream.render(
+
+
+ ,
)
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('initial')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
-
- // should not call queryFn because of staleTime + initialData combo
- expect(queryFn).toHaveBeenCalledTimes(0)
- })
-
- it('should work with static placeholderData', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ withinDOM().getByText('loading 2...')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
- return <>{data}>
- }
- function Loading() {
- useTrackRenders()
-
- return <>loading..>
- }
- function Page() {
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- return 'test'
- },
- placeholderData: 'placeholder',
- })
- useTrackRenders()
+ deferred.resolve()
- return (
- }>
-
-
- )
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('placeholder')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
- })
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('testtesttesttesttest')
+ }
- it('should work with placeholderData: keepPreviousData', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
-
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
-
- return <>{data}>
- }
- function Loading() {
- useTrackRenders()
-
- return <>loading..>
- }
- function Page() {
- useTrackRenders()
- const [count, setCount] = React.useState(0)
- const query = useQuery({
- queryKey: [...key, count],
- queryFn: async () => {
- await sleep(1)
- return 'test-' + count
- },
- placeholderData: keepPreviousData,
- })
+ expect(callCount).toBe(1)
+ })
- return (
-
+ it('should work with initial data', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+ function Loading() {
+ useTrackRenders()
+
+ return <>loading..>
+ }
+ function Page() {
+ useTrackRenders()
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(1)
+ return 'test'
+ },
+ initialData: 'initial',
+ })
+
+ return (
}>
-
-
- )
- }
-
- const rendered = await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test-0')
- expect(renderedComponents).toEqual([MyComponent])
- }
-
- rendered.getByRole('button', { name: 'increment' }).click()
-
- // re-render because of the increment
- {
- const { renderedComponents } = await renderStream.takeRender()
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
-
- // re-render with new data, no loading between
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test-1')
- // no more suspense boundary rendering
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
- })
-
- it('should be possible to select a part of the data with select', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
+ )
+ }
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
- return <>{data}>
- }
-
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
-
- function Page() {
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- return { name: 'test' }
- },
- select: (data) => data.name,
- })
-
- useTrackRenders()
- return (
- }>
-
-
- )
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test')
- expect(renderedComponents).toEqual([MyComponent])
- }
- })
-
- it('should throw error if the promise fails', async () => {
- const renderStream = createRenderStream({ snapshotDOM: true })
- const consoleMock = vi
- .spyOn(console, 'error')
- .mockImplementation(() => undefined)
-
- const key = queryKey()
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
-
- return <>{data}>
- }
-
- function Loading() {
- return <>loading..>
- }
-
- let queryCount = 0
- function Page() {
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- if (++queryCount > 1) {
- // second time this query mounts, it should not throw
- return 'data'
- }
- throw new Error('Error test')
- },
- retry: false,
- })
-
- return (
- }>
-
-
+ await renderStream.render(
+
+
+ ,
)
- }
-
- const rendered = await renderStream.render(
-
-
- {({ reset }) => (
- (
-
-
error boundary
-
-
- )}
- >
-
-
- )}
-
- ,
- )
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('error boundary')
- }
-
- consoleMock.mockRestore()
-
- rendered.getByText('resetErrorBoundary').click()
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('data')
- }
-
- expect(queryCount).toBe(2)
- })
- it('should throw error if the promise fails (colocate suspense and promise)', async () => {
- const renderStream = createRenderStream({ snapshotDOM: true })
- const consoleMock = vi
- .spyOn(console, 'error')
- .mockImplementation(() => undefined)
-
- const key = queryKey()
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('initial')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+ })
- function MyComponent() {
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- throw new Error('Error test')
- },
- retry: false,
+ it('should not fetch with initial data and staleTime', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const queryFn = vi.fn().mockImplementation(async () => {
+ await sleep(1)
+ return 'test'
})
- const data = React.use(query.promise)
- return <>{data}>
- }
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+ function Loading() {
+ useTrackRenders()
+ return <>loading..>
+ }
+ function Page() {
+ useTrackRenders()
+ const query = useQuery({
+ queryKey: key,
+ queryFn,
+ initialData: 'initial',
+ staleTime: 1000,
+ })
+
+ return (
+ }>
+
+
+ )
+ }
- function Page() {
- return (
-
-
-
+ await renderStream.render(
+
+
+ ,
)
- }
- await renderStream.render(
-
- error boundary
}>
-
-
- ,
- )
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('initial')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
+ // should not call queryFn because of staleTime + initialData combo
+ expect(queryFn).toHaveBeenCalledTimes(0)
+ })
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('error boundary')
- }
+ it('should work with static placeholderData', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+ function Loading() {
+ useTrackRenders()
+
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(1)
+ return 'test'
+ },
+ placeholderData: 'placeholder',
+ })
+ useTrackRenders()
+
+ return (
+ }>
+
+
+ )
+ }
- consoleMock.mockRestore()
- })
+ await renderStream.render(
+
+
+ ,
+ )
- it('should recreate promise with data changes', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('placeholder')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+ })
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
+ it('should work with placeholderData: keepPreviousData', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+ function Loading() {
+ useTrackRenders()
+
+ return <>loading..>
+ }
+ function Page() {
+ useTrackRenders()
+ const [count, setCount] = React.useState(0)
+ const query = useQuery({
+ queryKey: [...key, count],
+ queryFn: async () => {
+ await sleep(1)
+ return 'test-' + count
+ },
+ placeholderData: keepPreviousData,
+ })
+
+ return (
+
+ }>
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
+ )
- return <>{data}>
- }
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test-0')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
+
+ rendered.getByRole('button', { name: 'increment' }).click()
+
+ // re-render because of the increment
+ {
+ const { renderedComponents } = await renderStream.takeRender()
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+
+ // re-render with new data, no loading between
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test-1')
+ // no more suspense boundary rendering
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+ })
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
- function Page() {
- const query = useQuery({
- queryKey: key,
- queryFn: async () => {
- await sleep(1)
- return 'test1'
- },
- })
+ it('should be possible to select a part of the data with select', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+ return <>{data}>
+ }
+
+ function Loading() {
+ useTrackRenders()
+ return <>loading..>
+ }
+
+ function Page() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(1)
+ return { name: 'test' }
+ },
+ select: (data) => data.name,
+ })
+
+ useTrackRenders()
+ return (
+ }>
+
+
+ )
+ }
- useTrackRenders()
- return (
- }>
-
-
+ await renderStream.render(
+
+
+ ,
)
- }
-
- await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test1')
- expect(renderedComponents).toEqual([MyComponent])
- }
-
- queryClient.setQueryData(key, 'test2')
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test2')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
- })
- it('should dedupe when re-fetched with queryClient.fetchQuery while suspending', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
- const queryFn = vi.fn().mockImplementation(async () => {
- await sleep(10)
- return 'test'
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
})
- const options = {
- queryKey: key,
- queryFn,
- }
+ it('should throw error if the promise fails', async () => {
+ let deferred = createDeferred()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const consoleMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => undefined)
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
+ const key = queryKey()
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
- return <>{data}>
- }
+ return <>{data}>
+ }
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const query = useQuery(options)
+ function Loading() {
+ return <>loading..>
+ }
- return (
-
+ let queryCount = 0
+ function Page() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ queryCount++
+ await deferred.promise
+
+ return 'data'
+ },
+ retry: false,
+ })
+
+ return (
}>
-
-
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ {({ reset }) => (
+ (
+
+
error boundary
+
+
+ )}
+ >
+
+
+ )}
+
+ ,
)
- }
- const rendered = await renderStream.render(
-
-
- ,
- )
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
+ deferred.reject(new Error('Error test'))
+ deferred = createDeferred()
- rendered.getByText('fetch').click()
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('error boundary')
+ }
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test')
- }
+ consoleMock.mockRestore()
- expect(queryFn).toHaveBeenCalledOnce()
- })
+ rendered.getByText('resetErrorBoundary').click()
- it('should dedupe when re-fetched with refetchQueries while suspending', async () => {
- const key = queryKey()
- let count = 0
- const renderStream = createRenderStream({ snapshotDOM: true })
- const queryFn = vi.fn().mockImplementation(async () => {
- await sleep(10)
- return 'test' + count++
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
+
+ deferred.resolve()
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('data')
+ }
+
+ expect(queryCount).toBe(2)
})
- const options = {
- queryKey: key,
- queryFn,
- }
+ it('should throw error if the promise fails (colocate suspense and promise)', async () => {
+ const deferred = createDeferred()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const consoleMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => undefined)
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
+ const key = queryKey()
- return <>{data}>
- }
+ function MyComponent() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await deferred.promise
+ return 'data'
+ },
+ retry: false,
+ })
+ const data = React.use(query.promise)
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const query = useQuery(options)
+ return <>{data}>
+ }
- return (
-
- }>
-
+ function Page() {
+ return (
+
+
-
-
+ )
+ }
+
+ await renderStream.render(
+
+ error boundary
}>
+
+
+ ,
)
- }
-
- const rendered = await renderStream.render(
-
-
- ,
- )
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- rendered.getByText('refetch').click()
+ deferred.reject(new Error('Error test'))
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test0')
- }
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('error boundary')
+ }
- expect(queryFn).toHaveBeenCalledOnce()
- })
-
- it('should stay pending when canceled with cancelQueries while suspending until refetched', async () => {
- const renderStream = createRenderStream({ snapshotDOM: true })
- const key = queryKey()
- let count = 0
- const queryFn = vi.fn().mockImplementation(async () => {
- await sleep(10)
- return 'test' + count++
+ consoleMock.mockRestore()
})
- const options = {
- queryKey: key,
- queryFn,
- }
-
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
-
- return <>{data}>
- }
-
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const query = useQuery(options)
-
- return (
-
+ it('should recreate promise with data changes', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ useTrackRenders()
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn: async () => {
+ await sleep(1)
+ return 'test1'
+ },
+ })
+
+ useTrackRenders()
+ return (
}>
-
-
-
- )
- }
+ )
+ }
- const rendered = await renderStream.render(
-
- <>error boundary>}>
+ await renderStream.render(
+
-
- ,
- )
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
+ ,
+ )
- rendered.getByText('cancel').click()
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test1')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
+
+ queryClient.setQueryData(key, 'test2')
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test2')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+ })
- {
- await renderStream.takeRender()
- expect(queryClient.getQueryState(key)).toMatchObject({
- status: 'pending',
- fetchStatus: 'idle',
+ it('should dedupe when re-fetched with queryClient.fetchQuery while suspending', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const queryFn = vi.fn().mockImplementation(async () => {
+ await sleep(10)
+ return 'test'
})
- }
- expect(queryFn).toHaveBeenCalledOnce()
-
- rendered.getByText('fetch').click()
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('hello')
- }
- })
+ const options = {
+ queryKey: key,
+ queryFn,
+ }
+
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery(options)
+
+ return (
+
+ }>
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
+ )
- it('should resolve to previous data when canceled with cancelQueries while suspending', async () => {
- const renderStream = createRenderStream({ snapshotDOM: true })
- const key = queryKey()
- const queryFn = vi.fn().mockImplementation(async () => {
- await sleep(10)
- return 'test'
- })
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- const options = {
- queryKey: key,
- queryFn,
- }
+ rendered.getByText('fetch').click()
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test')
+ }
- return <>{data}>
- }
+ expect(queryFn).toHaveBeenCalledOnce()
+ })
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const query = useQuery(options)
+ it('should dedupe when re-fetched with refetchQueries while suspending', async () => {
+ const key = queryKey()
+ let count = 0
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const queryFn = vi.fn().mockImplementation(async () => {
+ await sleep(10)
+ return 'test' + count++
+ })
- return (
-
- }>
-
-
-
-
+ const options = {
+ queryKey: key,
+ queryFn,
+ }
+
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery(options)
+
+ return (
+
+ }>
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
)
- }
-
- queryClient.setQueryData(key, 'initial')
- const rendered = await renderStream.render(
-
-
- ,
- )
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- rendered.getByText('cancel').click()
+ rendered.getByText('refetch').click()
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('initial')
- }
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test0')
+ }
- expect(queryFn).toHaveBeenCalledTimes(1)
- })
-
- it('should suspend when not enabled', async () => {
- const renderStream = createRenderStream({ snapshotDOM: true })
- const key = queryKey()
-
- const options = (count: number) => ({
- queryKey: [...key, count],
- queryFn: async () => {
- await sleep(10)
- return 'test' + count
- },
+ expect(queryFn).toHaveBeenCalledOnce()
})
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
-
- return <>{data}>
- }
-
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const [count, setCount] = React.useState(0)
- const query = useQuery({ ...options(count), enabled: count > 0 })
+ it('should stay pending when canceled with cancelQueries while suspending until refetched', async () => {
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const key = queryKey()
+ let count = 0
+ const queryFn = vi.fn().mockImplementation(async () => {
+ await sleep(10)
+ return 'test' + count++
+ })
- return (
-
- }>
-
-
-
-
+ const options = {
+ queryKey: key,
+ queryFn,
+ }
+
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery(options)
+
+ return (
+
+ }>
+
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+ <>error boundary>}>
+
+
+ ,
)
- }
- const rendered = await renderStream.render(
-
-
- ,
- )
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
+ rendered.getByText('cancel').click()
- rendered.getByText('enable').click()
+ {
+ await renderStream.takeRender()
+ expect(queryClient.getQueryState(key)).toMatchObject({
+ status: 'pending',
+ fetchStatus: 'idle',
+ })
+ }
- // loading re-render with enabled
- await renderStream.takeRender()
+ expect(queryFn).toHaveBeenCalledOnce()
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test1')
- }
- })
+ rendered.getByText('fetch').click()
- it('should show correct data when read from cache only (staleTime)', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
- queryClient.setQueryData(key, 'initial')
-
- const queryFn = vi.fn().mockImplementation(async () => {
- await sleep(1)
- return 'test'
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('hello')
+ }
})
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
-
- return <>{data}>
- }
+ it('should resolve to previous data when canceled with cancelQueries while suspending', async () => {
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const key = queryKey()
+ const queryFn = vi.fn().mockImplementation(async () => {
+ await sleep(10)
+ return 'test'
+ })
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const query = useQuery({
+ const options = {
queryKey: key,
queryFn,
- staleTime: Infinity,
- })
-
- return (
- }>
-
-
+ }
+
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery(options)
+
+ return (
+
+ }>
+
+
+
+
+ )
+ }
+
+ queryClient.setQueryData(key, 'initial')
+
+ const rendered = await renderStream.render(
+
+
+ ,
)
- }
- await renderStream.render(
-
-
- ,
- )
+ rendered.getByText('cancel').click()
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('initial')
- }
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('initial')
+ }
- expect(queryFn).toHaveBeenCalledTimes(0)
- })
+ expect(queryFn).toHaveBeenCalledTimes(1)
+ })
+
+ it('should suspend when not enabled', async () => {
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ const key = queryKey()
- it('should show correct data when switching between cache entries without re-fetches', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
-
- function MyComponent(props: { promise: Promise }) {
- useTrackRenders()
- const data = React.use(props.promise)
-
- return <>{data}>
- }
-
- function Loading() {
- useTrackRenders()
- return <>loading..>
- }
- function Page() {
- useTrackRenders()
- const [count, setCount] = React.useState(0)
- const query = useQuery({
- queryKey: [key, count],
+ const options = (count: number) => ({
+ queryKey: [...key, count],
queryFn: async () => {
await sleep(10)
return 'test' + count
},
- staleTime: Infinity,
})
- return (
-
- }>
-
-
-
-
-
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const [count, setCount] = React.useState(0)
+ const query = useQuery({ ...options(count), enabled: count > 0 })
+
+ return (
+
+ }>
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
)
- }
-
- const rendered = await renderStream.render(
-
-
- ,
- )
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test0')
- expect(renderedComponents).toEqual([MyComponent])
- }
-
- rendered.getByText('inc').click()
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(renderedComponents).toEqual([Page, Loading])
- }
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test1')
- expect(renderedComponents).toEqual([MyComponent])
- }
-
- rendered.getByText('dec').click()
-
- {
- const { renderedComponents, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test0')
- expect(renderedComponents).toEqual([Page, MyComponent])
- }
- })
- it('should not resolve with intermediate data when keys are switched', async () => {
- const key = queryKey()
- const renderStream = createRenderStream<{ data: string }>({
- snapshotDOM: true,
- })
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
+ rendered.getByText('enable').click()
- renderStream.replaceSnapshot({ data })
+ // loading re-render with enabled
+ await renderStream.takeRender()
- return <>{data}>
- }
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test1')
+ }
+ })
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const [count, setCount] = React.useState(0)
- const query = useQuery({
- queryKey: [key, count],
- queryFn: async () => {
- await sleep(10)
- return 'test' + count
- },
- staleTime: Infinity,
+ it('should show correct data when read from cache only (staleTime)', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+ queryClient.setQueryData(key, 'initial')
+
+ const queryFn = vi.fn().mockImplementation(async () => {
+ await sleep(1)
+ return 'test'
})
- return (
-
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const query = useQuery({
+ queryKey: key,
+ queryFn,
+ staleTime: Infinity,
+ })
+
+ return (
}>
-
-
+ )
+ }
+
+ await renderStream.render(
+
+
+ ,
)
- }
-
- const rendered = await renderStream.render(
-
-
- ,
- )
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- {
- const { snapshot, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test0')
- expect(snapshot).toMatchObject({ data: 'test0' })
- }
-
- rendered.getByText('inc').click()
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- rendered.getByText('inc').click()
- await renderStream.takeRender()
-
- rendered.getByText('inc').click()
- await renderStream.takeRender()
-
- {
- const { snapshot, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test3')
- expect(snapshot).toMatchObject({ data: 'test3' })
- }
- })
- it('should not resolve with intermediate data when keys are switched (with background updates)', async () => {
- const key = queryKey()
- const renderStream = createRenderStream<{ data: string }>({
- snapshotDOM: true,
- })
- let modifier = ''
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('initial')
+ }
- function MyComponent(props: { promise: Promise }) {
- const data = React.use(props.promise)
+ expect(queryFn).toHaveBeenCalledTimes(0)
+ })
- renderStream.replaceSnapshot({ data })
+ it('should show correct data when switching between cache entries without re-fetches', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent(props: { promise: Promise }) {
+ useTrackRenders()
+ const data = React.use(props.promise)
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ useTrackRenders()
+ return <>loading..>
+ }
+ function Page() {
+ useTrackRenders()
+ const [count, setCount] = React.useState(0)
+ const query = useQuery({
+ queryKey: [key, count],
+ queryFn: async () => {
+ await sleep(10)
+ return 'test' + count
+ },
+ staleTime: Infinity,
+ })
+
+ return (
+
+ }>
+
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
+ )
- return <>{data}>
- }
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test0')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
+
+ rendered.getByText('inc').click()
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(renderedComponents).toEqual([Page, Loading])
+ }
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test1')
+ expect(renderedComponents).toEqual([MyComponent])
+ }
+
+ rendered.getByText('dec').click()
+
+ {
+ const { renderedComponents, withinDOM } =
+ await renderStream.takeRender()
+ withinDOM().getByText('test0')
+ expect(renderedComponents).toEqual([Page, MyComponent])
+ }
+ })
- function Loading() {
- return <>loading..>
- }
- function Page() {
- const [count, setCount] = React.useState(0)
- const query = useQuery({
- queryKey: [key, count],
- queryFn: async () => {
- await sleep(10)
- return 'test' + count + modifier
- },
+ it('should not resolve with intermediate data when keys are switched', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream<{ data: string }>({
+ snapshotDOM: true,
})
- return (
-
- }>
-
-
-
-
-
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ renderStream.replaceSnapshot({ data })
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const [count, setCount] = React.useState(0)
+ const query = useQuery({
+ queryKey: [key, count],
+ queryFn: async () => {
+ await sleep(10)
+ return 'test' + count
+ },
+ staleTime: Infinity,
+ })
+
+ return (
+
+ }>
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
)
- }
-
- const rendered = await renderStream.render(
-
-
- ,
- )
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- {
- const { snapshot, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test0')
- expect(snapshot).toMatchObject({ data: 'test0' })
- }
-
- rendered.getByText('inc').click()
- {
- const { snapshot } = await renderStream.takeRender()
- expect(snapshot).toMatchObject({ data: 'test0' })
- }
-
- rendered.getByText('inc').click()
- {
- const { snapshot } = await renderStream.takeRender()
- expect(snapshot).toMatchObject({ data: 'test0' })
- }
-
- rendered.getByText('inc').click()
-
- {
- const { snapshot, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- expect(snapshot).toMatchObject({ data: 'test0' })
- }
-
- {
- const { snapshot, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test3')
- expect(snapshot).toMatchObject({ data: 'test3' })
- }
-
- modifier = 'new'
-
- rendered.getByText('dec').click()
- {
- const { snapshot } = await renderStream.takeRender()
- expect(snapshot).toMatchObject({ data: 'test2' })
- }
-
- rendered.getByText('dec').click()
- {
- const { snapshot } = await renderStream.takeRender()
- expect(snapshot).toMatchObject({ data: 'test1' })
- }
-
- rendered.getByText('dec').click()
- {
- const { snapshot } = await renderStream.takeRender()
- expect(snapshot).toMatchObject({ data: 'test0' })
- }
-
- {
- const { snapshot, withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('test0new')
- expect(snapshot).toMatchObject({ data: 'test0new' })
- }
- })
- it('should not suspend indefinitely with multiple, nested observers)', async () => {
- const key = queryKey()
- const renderStream = createRenderStream({ snapshotDOM: true })
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
- function MyComponent({ input }: { input: string }) {
- const query = useTheQuery(input)
- const data = React.use(query.promise)
+ {
+ const { snapshot, withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test0')
+ expect(snapshot).toMatchObject({ data: 'test0' })
+ }
- return <>{data}>
- }
+ rendered.getByText('inc').click()
- function useTheQuery(input: string) {
- return useQuery({
- staleTime: Infinity,
- queryKey: [key, input],
- queryFn: async () => {
- await sleep(1)
- return input + ' response'
- },
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
+
+ rendered.getByText('inc').click()
+ await renderStream.takeRender()
+
+ rendered.getByText('inc').click()
+ await renderStream.takeRender()
+
+ {
+ const { snapshot, withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test3')
+ expect(snapshot).toMatchObject({ data: 'test3' })
+ }
+ })
+
+ it('should not resolve with intermediate data when keys are switched (with background updates)', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream<{ data: string }>({
+ snapshotDOM: true,
})
- }
+ let modifier = ''
+
+ function MyComponent(props: { promise: Promise }) {
+ const data = React.use(props.promise)
+
+ renderStream.replaceSnapshot({ data })
+
+ return <>{data}>
+ }
+
+ function Loading() {
+ return <>loading..>
+ }
+ function Page() {
+ const [count, setCount] = React.useState(0)
+ const query = useQuery({
+ queryKey: [key, count],
+ queryFn: async () => {
+ await sleep(10)
+ return 'test' + count + modifier
+ },
+ })
+
+ return (
+
+ }>
+
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
+ )
- function Page() {
- const [input, setInput] = React.useState('defaultInput')
- useTheQuery(input)
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
+
+ {
+ const { snapshot, withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test0')
+ expect(snapshot).toMatchObject({ data: 'test0' })
+ }
+
+ rendered.getByText('inc').click()
+ {
+ const { snapshot } = await renderStream.takeRender()
+ expect(snapshot).toMatchObject({ data: 'test0' })
+ }
+
+ rendered.getByText('inc').click()
+ {
+ const { snapshot } = await renderStream.takeRender()
+ expect(snapshot).toMatchObject({ data: 'test0' })
+ }
+
+ rendered.getByText('inc').click()
+
+ {
+ const { snapshot, withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ expect(snapshot).toMatchObject({ data: 'test0' })
+ }
+
+ {
+ const { snapshot, withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test3')
+ expect(snapshot).toMatchObject({ data: 'test3' })
+ }
+
+ modifier = 'new'
+
+ rendered.getByText('dec').click()
+ {
+ const { snapshot } = await renderStream.takeRender()
+ expect(snapshot).toMatchObject({ data: 'test2' })
+ }
+
+ rendered.getByText('dec').click()
+ {
+ const { snapshot } = await renderStream.takeRender()
+ expect(snapshot).toMatchObject({ data: 'test1' })
+ }
+
+ rendered.getByText('dec').click()
+ {
+ const { snapshot } = await renderStream.takeRender()
+ expect(snapshot).toMatchObject({ data: 'test0' })
+ }
+
+ {
+ const { snapshot, withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('test0new')
+ expect(snapshot).toMatchObject({ data: 'test0new' })
+ }
+ })
- return (
-
-
-
-
-
-
+ it('should not suspend indefinitely with multiple, nested observers)', async () => {
+ const key = queryKey()
+ const renderStream = createRenderStream({ snapshotDOM: true })
+
+ function MyComponent({ input }: { input: string }) {
+ const query = useTheQuery(input)
+ const data = React.use(query.promise)
+
+ return <>{data}>
+ }
+
+ function useTheQuery(input: string) {
+ return useQuery({
+ staleTime: Infinity,
+ queryKey: [key, input],
+ queryFn: async () => {
+ await sleep(1)
+ return input + ' response'
+ },
+ })
+ }
+
+ function Page() {
+ const [input, setInput] = React.useState('defaultInput')
+ useTheQuery(input)
+
+ return (
+
+
+
+
+
+
+ )
+ }
+
+ const rendered = await renderStream.render(
+
+
+ ,
)
- }
-
- const rendered = await renderStream.render(
-
-
- ,
- )
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('defaultInput response')
- }
-
- expect(
- queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })!
- .observers.length,
- ).toBe(2)
-
- rendered.getByText('setInput').click()
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('loading..')
- }
-
- {
- const { withinDOM } = await renderStream.takeRender()
- withinDOM().getByText('someInput response')
- }
-
- expect(
- queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })!
- .observers.length,
- ).toBe(0)
-
- expect(
- queryClient.getQueryCache().find({ queryKey: [key, 'someInput'] })!
- .observers.length,
- ).toBe(2)
+
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
+
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('defaultInput response')
+ }
+
+ expect(
+ queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })!
+ .observers.length,
+ ).toBe(2)
+
+ rendered.getByText('setInput').click()
+
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('loading..')
+ }
+
+ {
+ const { withinDOM } = await renderStream.takeRender()
+ withinDOM().getByText('someInput response')
+ }
+
+ expect(
+ queryClient.getQueryCache().find({ queryKey: [key, 'defaultInput'] })!
+ .observers.length,
+ ).toBe(0)
+
+ expect(
+ queryClient.getQueryCache().find({ queryKey: [key, 'someInput'] })!
+ .observers.length,
+ ).toBe(2)
+ })
})
})
diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx
index 9f0cbce408..a19047be86 100644
--- a/packages/react-query/src/__tests__/useQuery.test.tsx
+++ b/packages/react-query/src/__tests__/useQuery.test.tsx
@@ -6,6 +6,7 @@ import { dehydrate, hydrate, skipToken } from '@tanstack/query-core'
import { QueryCache, keepPreviousData, useQuery } from '..'
import {
Blink,
+ arrayPick,
createQueryClient,
mockOnlineManagerIsOnline,
mockVisibilityState,
@@ -2485,38 +2486,6 @@ describe('useQuery', () => {
expect(queryCache.find({ queryKey: key })!.options.retryDelay).toBe(20)
})
- it('should batch re-renders', async () => {
- const key = queryKey()
-
- let renders = 0
-
- const queryFn = async () => {
- await sleep(15)
- return 'data'
- }
-
- function Page() {
- const query1 = useQuery({ queryKey: key, queryFn })
- const query2 = useQuery({ queryKey: key, queryFn })
- renders++
-
- return (
-
- {query1.data} {query2.data}
-
- )
- }
-
- const rendered = renderWithClient(queryClient, )
-
- await waitFor(() => {
- rendered.getByText('data data')
- })
-
- // Should be 2 instead of 3
- expect(renders).toBe(2)
- })
-
it('should render latest data even if react has discarded certain renders', async () => {
const key = queryKey()
@@ -4803,6 +4772,7 @@ describe('useQuery', () => {
return count
},
staleTime: Infinity,
+ notifyOnChangeProps: 'all',
})
states.push(state)
@@ -4829,34 +4799,53 @@ describe('useQuery', () => {
expect(count).toBe(2)
- expect(states[0]).toMatchObject({
- data: undefined,
- isPending: true,
- isFetching: true,
- isSuccess: false,
- isStale: true,
- })
- expect(states[1]).toMatchObject({
- data: 1,
- isPending: false,
- isFetching: false,
- isSuccess: true,
- isStale: false,
- })
- expect(states[2]).toMatchObject({
- data: undefined,
- isPending: true,
- isFetching: true,
- isSuccess: false,
- isStale: true,
- })
- expect(states[3]).toMatchObject({
- data: 2,
- isPending: false,
- isFetching: false,
- isSuccess: true,
- isStale: false,
- })
+ expect(
+ arrayPick(states, [
+ 'data',
+ 'isStale',
+ 'isFetching',
+ 'isPending',
+ 'isSuccess',
+ ]),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "data": undefined,
+ "isFetching": true,
+ "isPending": true,
+ "isStale": true,
+ "isSuccess": false,
+ },
+ {
+ "data": 1,
+ "isFetching": false,
+ "isPending": false,
+ "isStale": false,
+ "isSuccess": true,
+ },
+ {
+ "data": undefined,
+ "isFetching": true,
+ "isPending": true,
+ "isStale": true,
+ "isSuccess": false,
+ },
+ {
+ "data": undefined,
+ "isFetching": true,
+ "isPending": true,
+ "isStale": true,
+ "isSuccess": false,
+ },
+ {
+ "data": 2,
+ "isFetching": false,
+ "isPending": false,
+ "isStale": false,
+ "isSuccess": true,
+ },
+ ]
+ `)
})
it('should update query state and not refetch when resetting a disabled query with resetQueries', async () => {
diff --git a/packages/react-query/src/__tests__/utils.tsx b/packages/react-query/src/__tests__/utils.tsx
index 7e25177768..dd2e7c13f8 100644
--- a/packages/react-query/src/__tests__/utils.tsx
+++ b/packages/react-query/src/__tests__/utils.tsx
@@ -94,3 +94,23 @@ export function setIsServer(isServer: boolean) {
}
export const doNotExecute = (_func: () => void) => true
+
+function pick(
+ obj: T,
+ keys: Array,
+): Pick {
+ return keys.reduce(
+ (acc, key) => {
+ acc[key] = obj[key]
+ return acc
+ },
+ {} as Pick,
+ )
+}
+
+export function arrayPick(
+ list: Array,
+ keys: Array,
+): Array> {
+ return list.map((item) => pick(item, keys))
+}
diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts
index bcbf700ef7..0438b362a6 100644
--- a/packages/react-query/src/useBaseQuery.ts
+++ b/packages/react-query/src/useBaseQuery.ts
@@ -82,33 +82,44 @@ export function useBaseQuery<
),
)
+ const [_, setForceUpdate] = React.useState({})
+
const result = observer.getOptimisticResult(defaultedOptions)
- React.useSyncExternalStore(
- React.useCallback(
- (onStoreChange) => {
- const unsubscribe = isRestoring
- ? noop
- : 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(() => {
+ if (isRestoring) {
+ return
+ }
+
+ const unsubscribe = observer.subscribe(
+ notifyManager.batchCalls(() => {
+ setForceUpdate({})
+ }),
+ )
+
+ // 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])
React.useEffect(() => {
+ if (defaultedOptions.experimental_prefetchInRender) {
+ return
+ }
// 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])
+ // For prefetchInRender, we need to set the options within the render
+ if (defaultedOptions.experimental_prefetchInRender) {
+ // 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 })
+ }
+
// Handle suspense
if (shouldSuspend(defaultedOptions, result)) {
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
diff --git a/packages/react-query/vite.config.ts b/packages/react-query/vite.config.ts
index fba5f8d044..9edede43c7 100644
--- a/packages/react-query/vite.config.ts
+++ b/packages/react-query/vite.config.ts
@@ -7,7 +7,6 @@ export default defineConfig({
test: {
name: packageJson.name,
dir: './src',
- watch: false,
environment: 'jsdom',
setupFiles: ['test-setup.ts'],
coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] },
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a0941c8880..20793c3f51 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1313,6 +1313,31 @@ importers:
specifier: ^5.3.5
version: 5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6)
+ examples/react/transition:
+ dependencies:
+ '@tanstack/react-query':
+ specifier: ^5.62.8
+ version: link:../../../packages/react-query
+ '@tanstack/react-query-devtools':
+ specifier: ^5.62.8
+ version: link:../../../packages/react-query-devtools
+ react:
+ specifier: ^19.0.0
+ version: 19.0.0
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.0.0(react@19.0.0)
+ devDependencies:
+ '@vitejs/plugin-react':
+ specifier: ^4.3.3
+ version: 4.3.3(vite@5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6))
+ typescript:
+ specifier: 5.7.2
+ version: 5.7.2
+ vite:
+ specifier: ^5.3.5
+ version: 5.4.11(@types/node@22.9.3)(less@4.2.1)(lightningcss@1.27.0)(sass@1.81.0)(terser@5.31.6)
+
examples/solid/astro:
dependencies:
'@astrojs/check':