From c4a8a1267e44b4d23fd44f4ed4a03fa8c0fb234d Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 23 Jan 2024 10:26:21 -0800 Subject: [PATCH 1/6] feat: add useQueryState hook --- packages/react-query/src/index.ts | 1 + packages/react-query/src/useIsFetching.ts | 21 ++----- packages/react-query/src/useQueryState.ts | 70 +++++++++++++++++++++++ 3 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 packages/react-query/src/useQueryState.ts diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index 926c673a6d..099ebeb9a4 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -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' diff --git a/packages/react-query/src/useIsFetching.ts b/packages/react-query/src/useIsFetching.ts index a6252912f2..cb7045d8d2 100644 --- a/packages/react-query/src/useIsFetching.ts +++ b/packages/react-query/src/useIsFetching.ts @@ -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 } diff --git a/packages/react-query/src/useQueryState.ts b/packages/react-query/src/useQueryState.ts new file mode 100644 index 0000000000..dc90e6d90a --- /dev/null +++ b/packages/react-query/src/useQueryState.ts @@ -0,0 +1,70 @@ +'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 = { + filters?: QueryFilters + select?: (query: Query) => TResult +} + +function getResult( + queryCache: QueryCache, + options: QueryStateOptions, +): Array { + return queryCache + .findAll(options.filters) + .map( + (query): TResult => + (options.select + ? options.select( + query as Query, + ) + : query.state) as TResult, + ) +} + +export function useQueryState( + options: QueryStateOptions = {}, + queryClient?: QueryClient, +): Array { + const queryCache = useQueryClient(queryClient).getQueryCache() + const optionsRef = React.useRef(options) + const result = React.useRef>() + if (!result.current) { + result.current = getResult(queryCache, options) + } + + React.useEffect(() => { + optionsRef.current = options + }) + + return React.useSyncExternalStore( + React.useCallback( + (onStoreChange) => + queryCache.subscribe(() => { + const nextResult = replaceEqualDeep( + result.current, + getResult(queryCache, optionsRef.current), + ) + if (result.current !== nextResult) { + result.current = nextResult + notifyManager.schedule(onStoreChange) + } + }), + [queryCache], + ), + () => result.current, + () => result.current, + )! +} From b8d68514fa38182426f2e959c2d40e63ec798af2 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 23 Jan 2024 13:22:11 -0800 Subject: [PATCH 2/6] test: improve useIsFetching tests --- .../src/__tests__/useIsFetching.test.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx index c80261c6b5..7eabbf2036 100644 --- a/packages/react-query/src/__tests__/useIsFetching.test.tsx +++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx @@ -176,11 +176,16 @@ describe('useIsFetching', () => { const queryClient = createQueryClient() const key = queryKey() + let resolve!: () => void + const promise = new Promise((_resolve) => { + resolve = _resolve + }) + function Page() { useQuery({ queryKey: key, queryFn: async () => { - await sleep(10) + await promise return 'test' }, }) @@ -197,6 +202,9 @@ describe('useIsFetching', () => { const rendered = renderWithClient(queryClient, ) await rendered.findByText('isFetching: 1') + + resolve() + await rendered.findByText('isFetching: 0') }) @@ -204,12 +212,17 @@ describe('useIsFetching', () => { const queryClient = createQueryClient() const key = queryKey() + let resolve!: () => void + const promise = new Promise((_resolve) => { + resolve = _resolve + }) + function Page() { useQuery( { queryKey: key, queryFn: async () => { - await sleep(10) + await promise return 'test' }, }, @@ -228,5 +241,6 @@ describe('useIsFetching', () => { const rendered = render() await waitFor(() => rendered.getByText('isFetching: 1')) + resolve() }) }) From 4f000317b618878aaabd91d21695f74ffcd0808f Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Wed, 24 Jan 2024 10:20:10 +0100 Subject: [PATCH 3/6] fix: useQueryState impl the value of the ref can get out of sync if we only write the ref in the subscribe function --- .../src/__tests__/useIsFetching.test.tsx | 22 +++----------- packages/react-query/src/useQueryState.ts | 29 +++++++++---------- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx index 7eabbf2036..aec5b027ee 100644 --- a/packages/react-query/src/__tests__/useIsFetching.test.tsx +++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx @@ -176,16 +176,11 @@ describe('useIsFetching', () => { const queryClient = createQueryClient() const key = queryKey() - let resolve!: () => void - const promise = new Promise((_resolve) => { - resolve = _resolve - }) - function Page() { useQuery({ queryKey: key, queryFn: async () => { - await promise + await sleep(100) return 'test' }, }) @@ -202,9 +197,6 @@ describe('useIsFetching', () => { const rendered = renderWithClient(queryClient, ) await rendered.findByText('isFetching: 1') - - resolve() - await rendered.findByText('isFetching: 0') }) @@ -212,25 +204,20 @@ describe('useIsFetching', () => { const queryClient = createQueryClient() const key = queryKey() - let resolve!: () => void - const promise = new Promise((_resolve) => { - resolve = _resolve - }) - function Page() { + const isFetching = useIsFetching({}, queryClient) + useQuery( { queryKey: key, queryFn: async () => { - await promise + await sleep(10) return 'test' }, }, queryClient, ) - const isFetching = useIsFetching({}, queryClient) - return (
isFetching: {isFetching}
@@ -241,6 +228,5 @@ describe('useIsFetching', () => { const rendered = render() await waitFor(() => rendered.getByText('isFetching: 1')) - resolve() }) }) diff --git a/packages/react-query/src/useQueryState.ts b/packages/react-query/src/useQueryState.ts index dc90e6d90a..81e722e092 100644 --- a/packages/react-query/src/useQueryState.ts +++ b/packages/react-query/src/useQueryState.ts @@ -26,11 +26,7 @@ function getResult( .findAll(options.filters) .map( (query): TResult => - (options.select - ? options.select( - query as Query, - ) - : query.state) as TResult, + (options.select ? options.select(query) : query.state) as TResult, ) } @@ -52,19 +48,20 @@ export function useQueryState( return React.useSyncExternalStore( React.useCallback( (onStoreChange) => - queryCache.subscribe(() => { - const nextResult = replaceEqualDeep( - result.current, - getResult(queryCache, optionsRef.current), - ) - if (result.current !== nextResult) { - result.current = nextResult - notifyManager.schedule(onStoreChange) - } - }), + queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), [queryCache], ), - () => result.current, + () => { + const nextResult = replaceEqualDeep( + result.current, + getResult(queryCache, optionsRef.current), + ) + if (result.current !== nextResult) { + result.current = nextResult + } + + return result.current + }, () => result.current, )! } From ff658ee94bcf657440968915939946e6a96e3c32 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 24 Jan 2024 05:31:21 -0800 Subject: [PATCH 4/6] chore: refactor useMutationState, fix tests --- .../src/__tests__/useMutationState.test.tsx | 9 ++++++-- packages/react-query/src/useMutationState.ts | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/react-query/src/__tests__/useMutationState.test.tsx b/packages/react-query/src/__tests__/useMutationState.test.tsx index 5f09a7aa17..643ced0c2a 100644 --- a/packages/react-query/src/__tests__/useMutationState.test.tsx +++ b/packages/react-query/src/__tests__/useMutationState.test.tsx @@ -66,12 +66,15 @@ describe('useIsMutating', () => { const isMutatings: Array = [] const queryClient = createQueryClient() - function IsMutating() { + function IsMutatingBase() { const isMutating = useIsMutating({ mutationKey: ['mutation1'] }) isMutatings.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'], @@ -104,7 +107,7 @@ describe('useIsMutating', () => { const isMutatings: Array = [] const queryClient = createQueryClient() - function IsMutating() { + function IsMutatingBase() { const isMutating = useIsMutating({ predicate: (mutation) => mutation.options.mutationKey?.[0] === 'mutation1', @@ -113,6 +116,8 @@ describe('useIsMutating', () => { return null } + const IsMutating = React.memo(IsMutatingBase); + function Page() { const { mutate: mutate1 } = useMutation({ mutationKey: ['mutation1'], diff --git a/packages/react-query/src/useMutationState.ts b/packages/react-query/src/useMutationState.ts index d14ebc46b7..61054a8180 100644 --- a/packages/react-query/src/useMutationState.ts +++ b/packages/react-query/src/useMutationState.ts @@ -64,19 +64,20 @@ export function useMutationState( 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, )! } From a1647943fe344ddf6c11e72d79a40ccca317ed8e Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Sat, 27 Jan 2024 14:45:52 +0100 Subject: [PATCH 5/6] chore: prettier --- packages/react-query/src/__tests__/useMutationState.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-query/src/__tests__/useMutationState.test.tsx b/packages/react-query/src/__tests__/useMutationState.test.tsx index 643ced0c2a..05261e1554 100644 --- a/packages/react-query/src/__tests__/useMutationState.test.tsx +++ b/packages/react-query/src/__tests__/useMutationState.test.tsx @@ -116,7 +116,7 @@ describe('useIsMutating', () => { return null } - const IsMutating = React.memo(IsMutatingBase); + const IsMutating = React.memo(IsMutatingBase) function Page() { const { mutate: mutate1 } = useMutation({ From 7bf9b4e646d1d47ff2d386c38a317d2bbfd0050f Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Sat, 27 Jan 2024 14:48:07 +0100 Subject: [PATCH 6/6] test: revert timeout changes --- packages/react-query/src/__tests__/useIsFetching.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx index aec5b027ee..fe47628a4b 100644 --- a/packages/react-query/src/__tests__/useIsFetching.test.tsx +++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx @@ -180,7 +180,7 @@ describe('useIsFetching', () => { useQuery({ queryKey: key, queryFn: async () => { - await sleep(100) + await sleep(10) return 'test' }, })