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
4 changes: 2 additions & 2 deletions packages/react-query/src/__tests__/useIsFetching.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ describe('useIsFetching', () => {
const key = queryKey()

function Page() {
const isFetching = useIsFetching({}, queryClient)

useQuery(
{
queryKey: key,
Expand All @@ -216,8 +218,6 @@ describe('useIsFetching', () => {
queryClient,
)

const isFetching = useIsFetching({}, queryClient)

return (
<div>
<div>isFetching: {isFetching}</div>
Expand Down
9 changes: 7 additions & 2 deletions packages/react-query/src/__tests__/useMutationState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ describe('useIsMutating', () => {
const isMutatingArray: Array<number> = []
const queryClient = createQueryClient()

function IsMutating() {
function IsMutatingBase() {
const isMutating = useIsMutating({ mutationKey: ['mutation1'] })
isMutatingArray.push(isMutating)
return null
}

// Memo to avoid other `useMutation` hook causing a re-render
const IsMutating = React.memo(IsMutatingBase)

function Page() {
const { mutate: mutate1 } = useMutation({
mutationKey: ['mutation1'],
Expand Down Expand Up @@ -104,7 +107,7 @@ describe('useIsMutating', () => {
const isMutatingArray: Array<number> = []
const queryClient = createQueryClient()

function IsMutating() {
function IsMutatingBase() {
const isMutating = useIsMutating({
predicate: (mutation) =>
mutation.options.mutationKey?.[0] === 'mutation1',
Expand All @@ -113,6 +116,8 @@ describe('useIsMutating', () => {
return null
}

const IsMutating = React.memo(IsMutatingBase)

function Page() {
const { mutate: mutate1 } = useMutation({
mutationKey: ['mutation1'],
Expand Down
1 change: 1 addition & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export {
} from './QueryErrorResetBoundary'
export { useIsFetching } from './useIsFetching'
export { useIsMutating, useMutationState } from './useMutationState'
export { useQueryState } from './useQueryState'
export { useMutation } from './useMutation'
export { useInfiniteQuery } from './useInfiniteQuery'
export { useIsRestoring, IsRestoringProvider } from './isRestoring'
21 changes: 5 additions & 16 deletions packages/react-query/src/useIsFetching.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
'use client'
import * as React from 'react'
import { notifyManager } from '@tanstack/query-core'

import { useQueryClient } from './QueryClientProvider'
import { useQueryState } from './useQueryState'
import type { QueryClient, QueryFilters } from '@tanstack/query-core'

export function useIsFetching(
filters?: QueryFilters,
queryClient?: QueryClient,
): number {
const client = useQueryClient(queryClient)
const queryCache = client.getQueryCache()

return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[queryCache],
),
() => client.isFetching(filters),
() => client.isFetching(filters),
)
return useQueryState(
{ filters: { ...filters, fetchStatus: 'fetching' } },
queryClient,
).length
}
23 changes: 12 additions & 11 deletions packages/react-query/src/useMutationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,20 @@ export function useMutationState<TResult = MutationState>(
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
mutationCache.subscribe(() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
notifyManager.schedule(onStoreChange)
}
}),
mutationCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[mutationCache],
),
() => result.current,
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(mutationCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}

return result.current
},
() => result.current,
)!
}
67 changes: 67 additions & 0 deletions packages/react-query/src/useQueryState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'
import * as React from 'react'

import { notifyManager, replaceEqualDeep } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import type {
DefaultError,
Query,
QueryCache,
QueryClient,
QueryFilters,
QueryKey,
QueryState,
} from '@tanstack/query-core'

type QueryStateOptions<TResult = QueryState> = {
filters?: QueryFilters
select?: (query: Query<unknown, DefaultError, unknown, QueryKey>) => TResult
}

function getResult<TResult = QueryState>(
queryCache: QueryCache,
options: QueryStateOptions<TResult>,
): Array<TResult> {
return queryCache
.findAll(options.filters)
.map(
(query): TResult =>
(options.select ? options.select(query) : query.state) as TResult,
)
}

export function useQueryState<TResult = QueryState>(
options: QueryStateOptions<TResult> = {},
queryClient?: QueryClient,
): Array<TResult> {
Comment on lines +33 to +36

Choose a reason for hiding this comment

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

medium

The current typings for useQueryState can lead to type-safety issues. If a consumer provides a generic type for TResult but omits the select function, the hook will return an array of QueryState objects that are incorrectly typed as TResult[]. This can lead to runtime errors that are not caught by the TypeScript compiler.

To improve type safety, I recommend using function overloads to ensure that if a custom TResult is specified, a select function returning that type is also required. This pattern is used elsewhere in React Query and would make this new API more robust.

const queryCache = useQueryClient(queryClient).getQueryCache()
const optionsRef = React.useRef(options)
const result = React.useRef<Array<TResult>>()
if (!result.current) {
result.current = getResult(queryCache, options)
}
Comment on lines +39 to +42
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Build break: useRef called without initial value (TS2554)

TS error matches pipeline: Expected 1 arguments, but got 0. Initialize the ref to null (or undefined) and keep the first‑render assignment.

Apply this diff:

-  const result = React.useRef<Array<TResult>>()
+  const result = React.useRef<Array<TResult> | null>(null)
   if (!result.current) {
     result.current = getResult(queryCache, options)
   }

This preserves runtime behavior and fixes typing by using non-null assertion on the hook return.

📝 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
const result = React.useRef<Array<TResult>>()
if (!result.current) {
result.current = getResult(queryCache, options)
}
const result = React.useRef<Array<TResult> | null>(null)
if (!result.current) {
result.current = getResult(queryCache, options)
}
🧰 Tools
🪛 GitHub Actions: pr

[error] 39-39: src/useQueryState.ts(39,24): TS2554: Expected 1 arguments, but got 0.

🤖 Prompt for AI Agents
In packages/react-query/src/useQueryState.ts around lines 39 to 42, useRef is
called with no initial value which breaks TS (expects 1 arg); change the ref to
be initialized to null (e.g., useRef<Array<TResult> | null>(null)) so typing is
satisfied, keep the existing first-render assignment result.current =
getResult(...), and use a non-null assertion when reading result.current later
(result.current!) to preserve runtime behavior and satisfy TypeScript.


React.useEffect(() => {
optionsRef.current = options
}, [options])

return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
[queryCache],
),
() => {
const nextResult = replaceEqualDeep(
result.current,
getResult(queryCache, optionsRef.current),
)
if (result.current !== nextResult) {
result.current = nextResult
}

return result.current
},
() => result.current,
)!
}
Loading