diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 9f0cbce408..d0f8817912 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -5964,6 +5964,110 @@ describe('useQuery', () => { }) }) + describe('subscribed', () => { + it('should be able to toggle subscribed', async () => { + const key = queryKey() + const queryFn = vi.fn(async () => 'data') + function Page() { + const [subscribed, setSubscribed] = React.useState(true) + const { data } = useQuery({ + queryKey: key, + queryFn, + subscribed, + }) + return ( +
+ data: {data} + +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('data: data')) + + expect( + queryClient.getQueryCache().find({ queryKey: key })!.observers.length, + ).toBe(1) + + fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) + + expect( + queryClient.getQueryCache().find({ queryKey: key })!.observers.length, + ).toBe(0) + + expect(queryFn).toHaveBeenCalledTimes(1) + + fireEvent.click(rendered.getByRole('button', { name: 'toggle' })) + + // background refetch when we re-subscribe + await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2)) + expect( + queryClient.getQueryCache().find({ queryKey: key })!.observers.length, + ).toBe(1) + }) + + it('should not be attached to the query when subscribed is false', async () => { + const key = queryKey() + const queryFn = vi.fn(async () => 'data') + function Page() { + const { data } = useQuery({ + queryKey: key, + queryFn, + subscribed: false, + }) + return ( +
+ data: {data} +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('data:')) + + expect( + queryClient.getQueryCache().find({ queryKey: key })!.observers.length, + ).toBe(0) + + expect(queryFn).toHaveBeenCalledTimes(0) + }) + + it('should not re-render when data is added to the cache when subscribed is false', async () => { + const key = queryKey() + let renders = 0 + function Page() { + const { data } = useQuery({ + queryKey: key, + queryFn: async () => 'data', + subscribed: false, + }) + renders++ + return ( +
+ {data ? 'has data' + data : 'no data'} + +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + await waitFor(() => rendered.getByText('no data')) + + fireEvent.click(rendered.getByRole('button', { name: 'set data' })) + + await sleep(10) + + await waitFor(() => rendered.getByText('no data')) + + expect(renders).toBe(1) + }) + }) + it('should have status=error on mount when a query has failed', async () => { const key = queryKey() const states: Array> = [] diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index 9f6d7c315f..8bbb351a85 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -36,7 +36,13 @@ export interface UseBaseQueryOptions< TData, TQueryData, TQueryKey - > {} + > { + /** + * Set this to `false` to unsubscribe this observer from updates to the query cache. + * Defaults to `true`. + */ + subscribed?: boolean +} export type AnyUseQueryOptions = UseQueryOptions export interface UseQueryOptions< diff --git a/packages/react-query/src/useBaseQuery.ts b/packages/react-query/src/useBaseQuery.ts index bcbf700ef7..1ee2f1cd07 100644 --- a/packages/react-query/src/useBaseQuery.ts +++ b/packages/react-query/src/useBaseQuery.ts @@ -82,14 +82,16 @@ export function useBaseQuery< ), ) + // note: this must be called before useSyncExternalStore const result = observer.getOptimisticResult(defaultedOptions) + const shouldSubscribe = !isRestoring && options.subscribed !== false React.useSyncExternalStore( React.useCallback( (onStoreChange) => { - const unsubscribe = isRestoring - ? noop - : observer.subscribe(notifyManager.batchCalls(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. @@ -97,7 +99,7 @@ export function useBaseQuery< return unsubscribe }, - [observer, isRestoring], + [observer, shouldSubscribe], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(), diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 90ef2e32ad..dd4ac9f96e 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -47,7 +47,7 @@ type UseQueryOptionsForUseQueries< TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< UseQueryOptions, - 'placeholderData' + 'placeholderData' | 'subscribed' > & { placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction } @@ -231,6 +231,7 @@ export function useQueries< }: { queries: readonly [...QueriesOptions] combine?: (result: QueriesResults) => TCombinedResult + subscribed?: boolean }, queryClient?: QueryClient, ): TCombinedResult { @@ -271,19 +272,21 @@ export function useQueries< ), ) + // note: this must be called before useSyncExternalStore const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, (options as QueriesObserverOptions).combine, ) + const shouldSubscribe = !isRestoring && options.subscribed !== false React.useSyncExternalStore( React.useCallback( (onStoreChange) => - isRestoring - ? noop - : observer.subscribe(notifyManager.batchCalls(onStoreChange)), - [observer, isRestoring], + shouldSubscribe + ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) + : noop, + [observer, shouldSubscribe], ), () => observer.getCurrentResult(), () => observer.getCurrentResult(),