Skip to content

[WIP] New useQueryState Hook #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add JSDoc documentation for useQueryState

This is a new public API, and it should have descriptive documentation to help users understand its purpose and usage.

Add JSDoc documentation:

+/**
+ * Hook that provides access to the state of queries in the cache.
+ *
+ * @template TResult The return type of the selection function, defaults to QueryState.
+ * @param options Configuration object to filter and transform query results
+ * @param queryClient Optional QueryClient instance, will use the default from context if not provided
+ * @returns An array of query states or selected results
+ *
+ * @example
+ * // Get all fetching queries
+ * const fetchingQueries = useQueryState({
+ *   filters: { fetchStatus: 'fetching' }
+ * })
+ *
+ * @example
+ * // Get only the data from specific queries
+ * const queryData = useQueryState({
+ *   filters: { queryKey: ['todos'] },
+ *   select: (query) => query.state.data
+ * })
+ */
 export function useQueryState<TResult = QueryState>(
   options: QueryStateOptions<TResult> = {},
   queryClient?: QueryClient,
 ): Array<TResult> {
📝 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
export function useQueryState<TResult = QueryState>(
options: QueryStateOptions<TResult> = {},
queryClient?: QueryClient,
): Array<TResult> {
/**
* Hook that provides access to the state of queries in the cache.
*
* @template TResult The return type of the selection function, defaults to QueryState.
* @param options Configuration object to filter and transform query results
* @param queryClient Optional QueryClient instance, will use the default from context if not provided
* @returns An array of query states or selected results
*
* @example
* // Get all fetching queries
* const fetchingQueries = useQueryState({
* filters: { fetchStatus: 'fetching' }
* })
*
* @example
* // Get only the data from specific queries
* const queryData = useQueryState({
* filters: { queryKey: ['todos'] },
* select: (query) => query.state.data
* })
*/
export function useQueryState<TResult = QueryState>(
options: QueryStateOptions<TResult> = {},
queryClient?: QueryClient,
): Array<TResult> {

const queryCache = useQueryClient(queryClient).getQueryCache()
const optionsRef = React.useRef(options)
const result = React.useRef<Array<TResult>>()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix TypeScript error: Initial value missing for useRef

The TypeScript error indicates that React.useRef expects an initial value, but none is provided here. This is causing the build failure.

Apply this fix:

-  const result = React.useRef<Array<TResult>>()
+  const result = React.useRef<Array<TResult>>([])
📝 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>>()
const result = React.useRef<Array<TResult>>([])
🧰 Tools
🪛 GitHub Actions: pr

[error] 39-39: TypeScript error TS2554: Expected 1 arguments, but got 0.

if (!result.current) {
result.current = getResult(queryCache, options)
}
Comment on lines +40 to +42
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Revisit initialization logic

Since you're initializing the result.current conditionally, this creates a potential race condition where the initial state might be undefined if accessed during the first render cycle before this check runs.

Consider either:

  1. Setting the initial value directly in the useRef call (like the fix above), or
  2. Add a null check when returning the result to handle potential undefined value
-  if (!result.current) {
-    result.current = getResult(queryCache, options)
-  }
+  // Initialize with current results when ref is empty
+  result.current = result.current || getResult(queryCache, options)
📝 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 (!result.current) {
result.current = getResult(queryCache, options)
}
// Initialize with current results when ref is empty
result.current = result.current || getResult(queryCache, options)


React.useEffect(() => {
optionsRef.current = 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