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
37 changes: 36 additions & 1 deletion packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitFor } from '@testing-library/dom'
import {
afterEach,
beforeEach,
Expand All @@ -7,8 +8,8 @@ import {
test,
vi,
} from 'vitest'
import { waitFor } from '@testing-library/dom'
import { QueryObserver, focusManager } from '..'
import { pendingThenable } from '../thenable'
import { createQueryClient, queryKey, sleep } from './utils'
import type { QueryClient, QueryObserverResult } from '..'

Expand Down Expand Up @@ -1233,4 +1234,38 @@ describe('queryObserver', () => {

unsubscribe()
})

test('switching enabled state should reuse the same promise', async () => {
const key = queryKey()

const observer = new QueryObserver(queryClient, {
queryKey: key,
enabled: false,
queryFn: () => 'data',
})
const results: Array<QueryObserverResult> = []

const success = pendingThenable<void>()

const unsubscribe = observer.subscribe((result) => {
results.push(result)

if (result.status === 'success') {
success.resolve()
}
})

observer.setOptions({
queryKey: key,
queryFn: () => 'data',
enabled: true,
})

await success

unsubscribe()

const promises = new Set(results.map((result) => result.promise))
expect(promises.size).toBe(1)
})
})
68 changes: 68 additions & 0 deletions packages/react-query/src/__tests__/useQuery.promise.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1377,4 +1377,72 @@ describe('useQuery().promise', () => {
.observers.length,
).toBe(2)
})

it('should handle enabled state changes with suspense', async () => {
const key = queryKey()
const renderStream = createRenderStream({ snapshotDOM: true })
const queryFn = vi.fn(async () => {
await sleep(1)
return 'test'
})

function MyComponent(props: { enabled: boolean }) {
const query = useQuery({
queryKey: key,
queryFn,
enabled: props.enabled,
staleTime: Infinity,
})

const data = React.use(query.promise)
return <>{data}</>
}

function Loading() {
return <>loading..</>
}

function Page() {
const enabledState = React.useState(false)
const enabled = enabledState[0]
const setEnabled = enabledState[1]

return (
<div>
<button onClick={() => setEnabled(true)}>enable</button>
<React.Suspense fallback={<Loading />}>
<MyComponent enabled={enabled} />
</React.Suspense>
</div>
)
}

const rendered = await renderStream.render(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>,
)

{
const result = await renderStream.takeRender()
result.withinDOM().getByText('loading..')
}

expect(queryFn).toHaveBeenCalledTimes(0)
rendered.getByText('enable').click()

{
const result = await renderStream.takeRender()
result.withinDOM().getByText('loading..')
}

Comment on lines +1431 to +1438
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete Test Coverage

The test verifies queryFn is called exactly once, but doesn't test the case where enabled changes from true to false and back to true. This leaves a logical gap in test coverage since the promise reuse behavior isn't fully verified across multiple enabled state transitions.

Standards
  • Algorithm-Correctness-Test-Coverage
  • Logic-Verification-Edge-Cases

expect(queryFn).toHaveBeenCalledTimes(1)

{
const result = await renderStream.takeRender()
result.withinDOM().getByText('test')
}

expect(queryFn).toHaveBeenCalledTimes(1)
})
})
15 changes: 11 additions & 4 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ export function useBaseQuery<
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 cacheEntry = client.getQueryCache().get(defaultedOptions.queryHash)

const [observer] = React.useState(
() =>
Expand Down Expand Up @@ -143,7 +141,16 @@ export function useBaseQuery<
!isServer &&
willFetch(result, isRestoring)
) {
const promise = isNewCacheEntry
// This fetching in the render should likely be done as part of the getOptimisticResult() considering https://github.com/TanStack/query/issues/8507
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Issue: The condition for shouldFetch has been modified to check cache entry state, but the comment "This fetching in the render should likely be done as part of the getOptimisticResult() considering TanStack#8507" suggests this may be a temporary solution. Consider addressing this architectural improvement in a future PR to ensure proper separation of concerns and better maintainability.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline Comment Placement

The comment is placed directly before the implementation logic it describes, breaking the flow of code. Moving this comment above the entire block (before line 143) would improve readability while maintaining the context.

Standards
  • Clean-Code-Comments
  • Maintainability-Quality-Readability

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline Comment Clarity

The comment indicates a potential architectural improvement but doesn't explain why the current implementation is problematic. Adding context about the architectural concern would improve future maintainability when revisiting this code.

Standards
  • Clean-Code-Comments
  • Maintainability-Quality-Documentation

const state = cacheEntry?.state

const shouldFetch =
Comment on lines +144 to +147
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race Condition Risk

Insufficient state validation creates race condition when enabled changes. Concurrent renders could trigger multiple fetches with inconsistent state. Potential memory leaks and unpredictable component behavior.

Suggested change
// This fetching in the render should likely be done as part of the getOptimisticResult() considering https://github.com/TanStack/query/issues/8507
const state = cacheEntry?.state
const shouldFetch =
const promise = shouldFetch
? // 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
observer.getOptimisticResult(defaultedOptions).promise
Standards
  • CWE-362
  • OWASP-A06

!state ||
(state.data === undefined &&
state.status === 'pending' &&
state.fetchStatus === 'idle')
Comment on lines +147 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect Fetch Condition

The fetch condition logic has been changed from a simple cache entry check to a more complex state-based condition. However, the new condition will trigger a fetch when state.data is undefined AND status is pending AND fetchStatus is idle, which is too restrictive and may prevent necessary fetches in other valid scenarios.

    const shouldFetch =
      !state ||
      (state.data === undefined &&
        (state.status !== 'success' || state.fetchStatus === 'idle'))
Commitable Suggestion
Suggested change
const shouldFetch =
!state ||
(state.data === undefined &&
state.status === 'pending' &&
state.fetchStatus === 'idle')
const shouldFetch =, !state ||, (state.data === undefined &&, (state.status !== 'success' || state.fetchStatus === 'idle'))
Standards
  • Algorithm-Correctness-Conditional-Logic
  • Business-Rule-State-Management
  • Logic-Verification-Data-Flow

Comment on lines +147 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent Fetch Condition

The shouldFetch condition doesn't account for enabled state changes. When enabled changes from false to true, the condition may not trigger a fetch if state exists with pending status, causing stale or missing data. This creates inconsistent behavior when toggling enabled state.

Standards
  • ISO-IEC-25010-Reliability-Maturity
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
  • SRE-Predictable-Behavior

Comment on lines +147 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant State Check

The condition checks for !state first, then accesses state properties without null check in the second condition. If state is null/undefined, the second condition would cause runtime errors. This creates potential for uncaught exceptions during state transitions.

Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
  • DbC-Preconditions

Comment on lines +147 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant State Check

The shouldFetch condition performs redundant checks that could be simplified. When state is undefined (!state), the subsequent condition is unnecessary. This creates extra boolean operations on every render affecting component performance under high frequency renders.

Standards
  • ISO-IEC-25010-Performance-Efficiency-Time-Behavior
  • Optimization-Pattern-Conditional-Logic
  • Algorithmic-Complexity-Boolean-Logic

Comment on lines +147 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary Fetch Condition

The shouldFetch condition checks for pending status but idle fetchStatus, which is contradictory. This could cause unnecessary fetches or missed fetches in edge cases, affecting query reliability and causing inconsistent behavior.

Standards
  • ISO-IEC-25010-Reliability-Maturity
  • ISO-IEC-25010-Functional-Correctness-Appropriateness

Comment on lines +144 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect Fetch Condition

The original condition used isNewCacheEntry to determine fetching, but the new logic checks state properties. This creates a logical inconsistency where a query with pending status but non-idle fetchStatus would be incorrectly skipped, potentially causing stale data to be used.

Standards
  • Algorithm-Correctness-Conditional-Logic
  • Business-Rule-State-Consistency


const promise = shouldFetch
Comment on lines +144 to +153
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent Fetch Condition

The condition for fetching changed from 'isNewCacheEntry' to 'shouldFetch', but the variable 'isNewCacheEntry' is no longer used. This creates inconsistency between variable declaration and usage, potentially causing unexpected behavior when the enabled state changes.

Standards
  • ISO-IEC-25010-Reliability-Maturity
  • ISO-IEC-25010-Functional-Correctness-Appropriateness

? // 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
Expand Down
Loading