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

Filter by extension

Filter by extension

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

Avoid fetching an outdated query key when an Activity tree is revealed after its query options changed while hidden.
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('PersistQueryClientProvider', () => {
await act(() => vi.advanceTimersByTimeAsync(11))
expect(rendered.getByText('fetched')).toBeInTheDocument()

expect(states).toHaveLength(4)
expect(states).toHaveLength(3)

expect(states[0]).toMatchObject({
status: 'pending',
Expand All @@ -125,12 +125,6 @@ describe('PersistQueryClientProvider', () => {
})

expect(states[2]).toMatchObject({
status: 'success',
fetchStatus: 'fetching',
data: 'hydrated',
})

expect(states[3]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'fetched',
Expand Down Expand Up @@ -343,7 +337,7 @@ describe('PersistQueryClientProvider', () => {
await act(() => vi.advanceTimersByTimeAsync(11))
expect(rendered.getByText('fetched')).toBeInTheDocument()

expect(states).toHaveLength(4)
expect(states).toHaveLength(3)

expect(states[0]).toMatchObject({
status: 'success',
Expand All @@ -358,12 +352,6 @@ describe('PersistQueryClientProvider', () => {
})

expect(states[2]).toMatchObject({
status: 'success',
fetchStatus: 'fetching',
data: 'hydrated',
})

expect(states[3]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'fetched',
Expand Down Expand Up @@ -704,7 +692,7 @@ describe('PersistQueryClientProvider', () => {
expect(queryFn2).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledTimes(1)

expect(states).toHaveLength(5)
expect(states).toHaveLength(4)

expect(states[0]).toMatchObject({
status: 'pending',
Expand All @@ -725,12 +713,6 @@ describe('PersistQueryClientProvider', () => {
})

expect(states[3]).toMatchObject({
status: 'success',
fetchStatus: 'fetching',
data: 'hydrated',
})

expect(states[4]).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'queryFn2',
Expand Down
121 changes: 121 additions & 0 deletions packages/react-query/src/__tests__/Activity.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act } from '@testing-library/react'
import * as React from 'react'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import {
QueryClient,
useQueries,
useQuery,
useSuspenseQueries,
useSuspenseQuery,
} from '..'
import { renderWithClient } from './utils'

type Options = {
queryKey: Array<string>
queryFn: () => Promise<string>
}

describe.each([
{
name: 'useQuery',
useTestQuery: (options: Options) => useQuery(options).data,
},
{
name: 'useSuspenseQuery',
useTestQuery: (options: Options) => useSuspenseQuery(options).data,
},
{
name: 'useQueries',
useTestQuery: (options: Options) =>
useQueries({ queries: [options] })[0].data,
},
{
name: 'useSuspenseQueries',
useTestQuery: (options: Options) =>
useSuspenseQueries({ queries: [options] })[0].data,
},
])('$name with Activity', ({ useTestQuery }) => {
let queryClient: QueryClient

beforeEach(() => {
vi.useFakeTimers()
queryClient = new QueryClient()
})

afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})

it('should not fetch the old key when revealed after a hidden key change', async () => {
const key = queryKey()
const fetched: Array<string> = []

function Page({ filter }: { filter: string }) {
const data = useTestQuery({
queryKey: [...key, filter],
queryFn: async () => {
fetched.push(filter)
await sleep(10)
return filter
},
})

return <p>data: {data}</p>
}

function App({
mode,
filter,
}: {
mode: 'visible' | 'hidden'
filter: string
}) {
return (
<React.Activity mode={mode}>
<React.Suspense fallback="loading">
<Page filter={filter} />
</React.Suspense>
</React.Activity>
)
}

const rendered = renderWithClient(
queryClient,
<App mode="visible" filter="a" />,
)
await act(() => vi.advanceTimersByTimeAsync(11))
expect(rendered.getByText('data: a')).toBeVisible()

rendered.rerender(<App mode="hidden" filter="b" />)
// Hidden trees render without running effects. Also let suspense's minimum
// staleTime elapse so that subscribing to the old key would refetch it.
await act(() => vi.advanceTimersByTimeAsync(1100))
expect(
queryClient
.getQueryCache()
.find({ queryKey: [...key, 'a'] })!
.getObserversCount(),
).toBe(0)
expect(
queryClient
.getQueryCache()
.find({ queryKey: [...key, 'b'] })!
.getObserversCount(),
).toBe(0)

rendered.rerender(<App mode="visible" filter="b" />)
await act(() => vi.advanceTimersByTimeAsync(50))

expect(rendered.getByText('data: b')).toBeVisible()
expect(fetched).toContain('b')
expect(fetched.filter((filter) => filter === 'a')).toStrictEqual(['a'])
expect(
queryClient
.getQueryCache()
.find({ queryKey: [...key, 'b'] })!
.getObserversCount(),
).toBe(1)
})
})
9 changes: 5 additions & 4 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export function useBaseQuery<
// note: this must be called before useSyncExternalStore
const result = observer.getOptimisticResult(defaultedOptions)

// Update options before subscribing so that an Activity reveal uses the latest query.
React.useEffect(() => {
observer.setOptions(defaultedOptions)
}, [defaultedOptions, observer])

const shouldSubscribe = !isRestoring && subscribed
React.useSyncExternalStore(
React.useCallback(
Expand All @@ -124,10 +129,6 @@ export function useBaseQuery<
() => observer.getCurrentResult(),
)

React.useEffect(() => {
observer.setOptions(defaultedOptions)
}, [defaultedOptions, observer])

// Handle suspense
if (shouldSuspend(defaultedOptions, result)) {
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
Expand Down
15 changes: 8 additions & 7 deletions packages/react-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ export function useQueries<
(options as QueriesObserverOptions<TCombinedResult>).combine,
)

// Update queries before subscribing so that an Activity reveal uses the latest queries.
React.useEffect(() => {
observer.setQueries(
defaultedQueries,
options as QueriesObserverOptions<TCombinedResult>,
)
}, [defaultedQueries, options, observer])

const shouldSubscribe = !isRestoring && subscribed
React.useSyncExternalStore(
React.useCallback(
Expand All @@ -444,13 +452,6 @@ export function useQueries<
() => observer.getCurrentResult(),
)

React.useEffect(() => {
observer.setQueries(
defaultedQueries,
options as QueriesObserverOptions<TCombinedResult>,
)
}, [defaultedQueries, options, observer])

const shouldAtLeastOneSuspend = optimisticResult.some((result, index) =>
shouldSuspend(defaultedQueries[index], result),
)
Expand Down