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
14 changes: 7 additions & 7 deletions packages/react-query/src/QueryErrorResetBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@ import * as React from 'react'

// CONTEXT
export type QueryErrorResetFunction = () => void
export type QueryErrorIsResetFunction = () => boolean
export type QueryErrorGetResetIdFunction = () => number
export type QueryErrorClearResetFunction = () => void

export interface QueryErrorResetBoundaryValue {
clearReset: QueryErrorClearResetFunction
isReset: QueryErrorIsResetFunction
getResetId: QueryErrorGetResetIdFunction
reset: QueryErrorResetFunction
}
Comment on lines 9 to 13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all usages of isReset() in the codebase to confirm incomplete migration

echo "=== Searching for isReset() calls ==="
rg -n 'isReset\(\)' --type ts

echo ""
echo "=== Searching for .isReset references ==="
rg -n '\.isReset' --type ts

Repository: TanStack/query

Length of output: 854


🏁 Script executed:

# Read the QueryErrorResetBoundary.tsx file to see the interface and implementation
cat -n packages/react-query/src/QueryErrorResetBoundary.tsx | head -40

Repository: TanStack/query

Length of output: 1296


🏁 Script executed:

# Read errorBoundaryUtils.ts to see how isReset() is being used
cat -n packages/react-query/src/errorBoundaryUtils.ts | head -100

Repository: TanStack/query

Length of output: 2682


Critical: Interface change breaks consumers in errorBoundaryUtils.ts.

The interface no longer exposes isReset() but only getResetId(). However, errorBoundaryUtils.ts still calls errorResetBoundary.isReset() at lines 41 and 76, and the same issue exists in preact-query/src/errorBoundaryUtils.ts at lines 35 and 70. This will cause a runtime error:

TypeError: errorResetBoundary.isReset is not a function

Update the consumer code to use the new getResetId() API with appropriate comparison logic, or keep isReset() in the interface alongside getResetId() for backward compatibility.


function createValue(): QueryErrorResetBoundaryValue {
let isReset = false
const resetIdRef = { current: 0 }
return {
clearReset: () => {
isReset = false
resetIdRef.current = 0
},
reset: () => {
isReset = true
resetIdRef.current += 1
},
isReset: () => {
return isReset
getResetId: () => {
return resetIdRef.current
},
}
}
Expand Down
73 changes: 13 additions & 60 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,75 +96,28 @@ export function useBaseQuery<
),
)

// note: this must be called before useSyncExternalStore
const result = observer.getOptimisticResult(defaultedOptions)
observer.setOptions(defaultedOptions, { isNewCacheEntry })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify setOptions signature in query-core

echo "=== setOptions method signature ==="
ast-grep --pattern 'setOptions($PARAM) {
  $$$
}'

Repository: TanStack/query

Length of output: 93


🏁 Script executed:

# Find the files mentioned
fd -type f -name "useBaseQuery.ts"
fd -type f -name "queryObserver.ts"

Repository: TanStack/query

Length of output: 399


🏁 Script executed:

# Find the files mentioned
fd "useBaseQuery.ts"
fd "queryObserver.ts"

Repository: TanStack/query

Length of output: 310


🏁 Script executed:

# Read useBaseQuery.ts from react-query around line 99
wc -l packages/react-query/src/useBaseQuery.ts
head -110 packages/react-query/src/useBaseQuery.ts | tail -20

Repository: TanStack/query

Length of output: 597


🏁 Script executed:

# Read queryObserver.ts around lines 138-176 to see setOptions signature
wc -l packages/query-core/src/queryObserver.ts
sed -n '130,180p' packages/query-core/src/queryObserver.ts

Repository: TanStack/query

Length of output: 1345


🏁 Script executed:

# Check lines 87-89 in useBaseQuery.ts to see the "dead code" mentioned
sed -n '85,92p' packages/react-query/src/useBaseQuery.ts

Repository: TanStack/query

Length of output: 295


🏁 Script executed:

# Search for all setOptions definitions to check for overloads or multiple implementations
rg "setOptions\s*\(" packages/query-core/src/queryObserver.ts

Repository: TanStack/query

Length of output: 148


🏁 Script executed:

# Check if isNewCacheEntry is used anywhere else in the file
rg "isNewCacheEntry" packages/react-query/src/useBaseQuery.ts -A 2 -B 2

Repository: TanStack/query

Length of output: 381


🏁 Script executed:

# Check if Observer class extends from QueryObserver or if there are other setOptions definitions
rg "class.*Observer" packages/query-core/src/queryObserver.ts -A 3

Repository: TanStack/query

Length of output: 160


🏁 Script executed:

# Check what Observer is imported as in useBaseQuery.ts
head -30 packages/react-query/src/useBaseQuery.ts | grep -E "^import|^export|Observer"

Repository: TanStack/query

Length of output: 501


🏁 Script executed:

# Verify the exact line numbers for context around the setOptions call
sed -n '85,105p' packages/react-query/src/useBaseQuery.ts | cat -n

Repository: TanStack/query

Length of output: 786


Remove second parameter from setOptions call or update the method signature to accept it.

observer.setOptions() in queryObserver.ts accepts only a single options parameter, but line 99 passes a second argument { isNewCacheEntry } which is silently ignored. The isNewCacheEntry value computed at lines 87-89 is never actually used.

Either:

  1. Update the setOptions signature in query-core to accept and handle isNewCacheEntry, or
  2. Remove the second parameter and handle isNewCacheEntry logic differently
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/useBaseQuery.ts` at line 99, The call
observer.setOptions(defaultedOptions, { isNewCacheEntry }) passes a second arg
that's ignored because setOptions in queryObserver.ts only accepts one
parameter; either extend setOptions in QueryObserver (queryObserver.ts) to
accept a second parameter (e.g., setOptions(options, { isNewCacheEntry }) ) and
have QueryObserver/observer logic consume that flag, or remove the second
parameter here in useBaseQuery.ts and ensure the isNewCacheEntry handling is
moved to where setOptions actually reads options (e.g., attach isNewCacheEntry
into defaultedOptions before calling setOptions or perform the related logic
immediately after computing isNewCacheEntry). Use the symbols
observer.setOptions, defaultedOptions, isNewCacheEntry and
QueryObserver/setOptions in your change to keep references clear.


const shouldSubscribe = !isRestoring && options.subscribed !== false
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => {
const unsubscribe = shouldSubscribe
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
: noop

// 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, shouldSubscribe],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
)
const result = observer.getOptimisticResult(defaultedOptions)

React.useEffect(() => {
observer.setOptions(defaultedOptions)
}, [defaultedOptions, observer])
errorResetBoundary.clearReset()
}, [errorResetBoundary])
Comment on lines 103 to +105
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Effect dependency may cause excessive clearReset calls.

The effect runs whenever errorResetBoundary changes. Since errorResetBoundary comes from useQueryErrorResetBoundary() (context), it should be stable. However, this unconditionally calls clearReset() on every mount, which resets the ID counter to 0.

This conflicts with the stated PR objective: if clearReset() is called on mount before queries can observe the reset ID, the reset signal may be lost before errored queries can act on it.

Consider whether this effect should only run under specific conditions (e.g., after a successful query render).

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/useBaseQuery.ts` around lines 103 - 105, The current
useBaseQuery effect always calls errorResetBoundary.clearReset() on mount/update
which can prematurely reset the error reset ID; modify useBaseQuery so
clearReset is only invoked when appropriate (e.g., after a successful query
render or when the query has transitioned to a non-error state) rather than
unconditionally on every mount/update. Locate the React.useEffect in
useBaseQuery that references errorResetBoundary and change the condition to
check the query's status/previous status (or a success flag) before calling
errorResetBoundary.clearReset(), ensuring errorResetBoundary (from
useQueryErrorResetBoundary) remains stable and that clearReset is only called
when queries are ready to observe the reset signal.


// Handle suspense
if (shouldSuspend(defaultedOptions, result)) {
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
}
React.useEffect(() => {
observer.subscribe(notifyManager.batchCalls(() => {}))
}, [observer])
Comment on lines +107 to +109
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Critical: Empty subscription callback prevents component re-renders.

The subscription callback is empty (() => {}), which means the component will never re-render when query state changes. This completely breaks the hook's reactivity.

The previous implementation likely used useSyncExternalStore or passed a callback that triggers re-renders (e.g., via forceUpdate or state setter). Without this, query data changes, loading states, and errors won't cause the component to update.

πŸ› Suggested fix pattern
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0)
+
  React.useEffect(() => {
-   observer.subscribe(notifyManager.batchCalls(() => {}))
+   const unsubscribe = observer.subscribe(
+     notifyManager.batchCalls(forceUpdate)
+   )
+   return unsubscribe
  }, [observer])

Also note: the effect is missing a cleanup function to unsubscribe when the component unmounts or observer changes.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/useBaseQuery.ts` around lines 107 - 109, The
useEffect in useBaseQuery currently calls
observer.subscribe(notifyManager.batchCalls(() => {})) which supplies an empty
callback so state changes never trigger re-renders; replace the empty function
with a real updater (e.g., a state setter or forceUpdate) wrapped by
notifyManager.batchCalls so the component re-renders when the observer notifies,
and add a cleanup that unsubscribes the returned subscription when the effect
tears down; locate useBaseQuery and update the observer.subscribe call and
effect cleanup accordingly.


// Handle error boundary
if (
getHasError({
result,
errorResetBoundary,
throwOnError: defaultedOptions.throwOnError,
query,
suspense: defaultedOptions.suspense,
})
) {
throw result.error
if (shouldSuspend(defaultedOptions, result, isRestoring)) {
return fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
Comment on lines +111 to +112
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify shouldSuspend signature

echo "=== shouldSuspend function signature ==="
rg -n -A 5 'export const shouldSuspend' packages/react-query/src/suspense.ts

Repository: TanStack/query

Length of output: 338


🏁 Script executed:

# Find useBaseQuery.ts and read the relevant lines
cd packages/react-query/src && cat -n useBaseQuery.ts | sed -n '100,125p'

Repository: TanStack/query

Length of output: 804


🏁 Script executed:

# Find fetchOptimistic function definition
rg -n -B 2 -A 10 'export.*fetchOptimistic|const fetchOptimistic' packages/react-query/src/

Repository: TanStack/query

Length of output: 837


🏁 Script executed:

# Check the return type of useBaseQuery hook
rg -n 'function useBaseQuery|const useBaseQuery' -A 3 packages/react-query/src/useBaseQuery.ts

Repository: TanStack/query

Length of output: 134


🏁 Script executed:

# Get complete fetchOptimistic function to see return type
rg -n -A 25 'export const fetchOptimistic = ' packages/react-query/src/suspense.ts

Repository: TanStack/query

Length of output: 595


🏁 Script executed:

# Get useBaseQuery full signature with return type
rg -n -A 15 'export function useBaseQuery' packages/react-query/src/useBaseQuery.ts

Repository: TanStack/query

Length of output: 388


🏁 Script executed:

# Get useBaseQuery return type (should follow the parameter list)
cat -n packages/react-query/src/useBaseQuery.ts | sed -n '27,50p'

Repository: TanStack/query

Length of output: 997


Arity and return type mismatches prevent this code from compiling.

Line 111-112 contains two critical issues:

  1. Arity mismatch: shouldSuspend accepts exactly 2 parameters (defaultedOptions, result), but 3 are passed including isRestoring. TypeScript will error on the extra argument.

  2. Return type mismatch: useBaseQuery is declared to return QueryObserverResult<TData, TError> (line 43), but line 112 returns the result of fetchOptimistic, which returns a Promise. This violates the return type contract.

Additionally, for React Suspense to work correctly, the Promise must be thrown, not returned.

Fix
- if (shouldSuspend(defaultedOptions, result, isRestoring)) {
-   return fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
+ if (shouldSuspend(defaultedOptions, result) && !isRestoring) {
+   throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
  }

The isRestoring check must be separate since it's not part of shouldSuspend's signature. The Promise should be thrown to trigger React Suspense behavior.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (shouldSuspend(defaultedOptions, result, isRestoring)) {
return fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
if (shouldSuspend(defaultedOptions, result) && !isRestoring) {
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
}
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/useBaseQuery.ts` around lines 111 - 112, The call to
shouldSuspend currently passes an extra argument and returns a Promise from
fetchOptimistic which breaks the declared return type of useBaseQuery
(QueryObserverResult) and prevents React Suspense from working; fix it by first
checking isRestoring separately (don’t pass it into
shouldSuspend(defaultedOptions, result)), and when suspension is required call
fetchOptimistic(defaultedOptions, observer, errorResetBoundary) and throw the
resulting Promise (not return it) so the function maintains its synchronous
return type while enabling React Suspense; update the code paths around
shouldSuspend, isRestoring and fetchOptimistic in useBaseQuery accordingly.

}

;(client.getDefaultOptions().queries as any)?._experimental_afterQuery?.(
defaultedOptions,
result,
)

if (
defaultedOptions.experimental_prefetchInRender &&
!environmentManager.isServer() &&
willFetch(result, isRestoring)
willFetch(result, isRestoring) &&
defaultedOptions._optimisticResults !== 'optimistic'
) {
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
query?.promise

promise?.catch(noop).finally(() => {
// `.updateResult()` will trigger `.#currentThenable` to finalize
observer.updateResult()
})
observer.fetchOptimistic(defaultedOptions)
}

// Handle result property usage tracking
return !defaultedOptions.notifyOnChangeProps
? observer.trackResult(result)
: result
return result
}
Loading