diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts index 4bed721396..81cd2f9eab 100644 --- a/src/core/queryClient.ts +++ b/src/core/queryClient.ts @@ -238,8 +238,10 @@ export class QueryClient { const refetchFilters: QueryFilters = { ...filters, - active: filters.refetchActive ?? true, - inactive: filters.refetchInactive, + // if filters.refetchActive is not provided and filters.active is explicitly false, + // e.g. invalidateQueries({ active: false }), we don't want to refetch active queries + active: filters.refetchActive ?? filters.active ?? true, + inactive: filters.refetchInactive ?? false, } return notifyManager.batch(() => { diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx index 0dca395e28..b192f943ee 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -649,9 +649,85 @@ describe('queryClient', () => { expect(queryFn1).toHaveBeenCalledTimes(2) expect(queryFn2).toHaveBeenCalledTimes(2) }) + + test('should be able to refetch only active queries', async () => { + const key1 = queryKey() + const key2 = queryKey() + const queryFn1 = jest.fn() + const queryFn2 = jest.fn() + await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: queryFn1, + staleTime: Infinity, + }) + const unsubscribe = observer.subscribe() + await queryClient.refetchQueries({ active: true }) + unsubscribe() + expect(queryFn1).toHaveBeenCalledTimes(2) + expect(queryFn2).toHaveBeenCalledTimes(1) + }) + + test('should be able to refetch only inactive queries', async () => { + const key1 = queryKey() + const key2 = queryKey() + const queryFn1 = jest.fn() + const queryFn2 = jest.fn() + await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: queryFn1, + staleTime: Infinity, + }) + const unsubscribe = observer.subscribe() + await queryClient.refetchQueries({ inactive: true }) + unsubscribe() + expect(queryFn1).toHaveBeenCalledTimes(1) + expect(queryFn2).toHaveBeenCalledTimes(2) + }) + + test('should skip refetch for all active and inactive queries', async () => { + const key1 = queryKey() + const key2 = queryKey() + const queryFn1 = jest.fn() + const queryFn2 = jest.fn() + await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: queryFn1, + staleTime: Infinity, + }) + const unsubscribe = observer.subscribe() + await queryClient.refetchQueries({ active: false, inactive: false }) + unsubscribe() + expect(queryFn1).toHaveBeenCalledTimes(1) + expect(queryFn2).toHaveBeenCalledTimes(1) + }) }) describe('invalidateQueries', () => { + test('should refetch active queries by default', async () => { + const key1 = queryKey() + const key2 = queryKey() + const queryFn1 = jest.fn() + const queryFn2 = jest.fn() + await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: queryFn1, + staleTime: Infinity, + }) + const unsubscribe = observer.subscribe() + queryClient.invalidateQueries(key1) + unsubscribe() + expect(queryFn1).toHaveBeenCalledTimes(2) + expect(queryFn2).toHaveBeenCalledTimes(1) + }) + test('should not refetch inactive queries by default', async () => { const key1 = queryKey() const key2 = queryKey() @@ -673,10 +749,14 @@ describe('queryClient', () => { test('should not refetch active queries when "refetchActive" is false', async () => { const key1 = queryKey() + const key2 = queryKey() const queryFn1 = jest.fn() + const queryFn2 = jest.fn() await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { queryKey: key1, + queryFn: queryFn1, staleTime: Infinity, }) const unsubscribe = observer.subscribe() @@ -685,6 +765,50 @@ describe('queryClient', () => { }) unsubscribe() expect(queryFn1).toHaveBeenCalledTimes(1) + expect(queryFn2).toHaveBeenCalledTimes(1) + }) + + test('should refetch inactive queries when "refetchInactive" is true', async () => { + const key1 = queryKey() + const key2 = queryKey() + const queryFn1 = jest.fn() + const queryFn2 = jest.fn() + await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: queryFn1, + staleTime: Infinity, + enabled: false, + }) + const unsubscribe = observer.subscribe() + queryClient.invalidateQueries(key1, { + refetchInactive: true, + }) + unsubscribe() + expect(queryFn1).toHaveBeenCalledTimes(2) + expect(queryFn2).toHaveBeenCalledTimes(1) + }) + + test('should not refetch active queries when "refetchActive" is not provided and "active" is false', async () => { + const key1 = queryKey() + const key2 = queryKey() + const queryFn1 = jest.fn() + const queryFn2 = jest.fn() + await queryClient.fetchQuery(key1, queryFn1) + await queryClient.fetchQuery(key2, queryFn2) + const observer = new QueryObserver(queryClient, { + queryKey: key1, + queryFn: queryFn1, + staleTime: Infinity, + }) + const unsubscribe = observer.subscribe() + queryClient.invalidateQueries(key1, { + active: false, + }) + unsubscribe() + expect(queryFn1).toHaveBeenCalledTimes(1) + expect(queryFn2).toHaveBeenCalledTimes(1) }) }) diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx index 9f1f5411b8..592f15b8b0 100644 --- a/src/core/tests/utils.test.tsx +++ b/src/core/tests/utils.test.tsx @@ -1,4 +1,9 @@ -import { replaceEqualDeep, partialDeepEqual, isPlainObject } from '../utils' +import { + replaceEqualDeep, + partialDeepEqual, + isPlainObject, + mapQueryStatusFilter, +} from '../utils' import { QueryClient, QueryCache, setLogger, Logger } from '../..' import { queryKey } from '../../react/tests/utils' @@ -302,4 +307,24 @@ describe('core/utils', () => { expect(result.todos[1]).toBe(prev.todos[1]) }) }) + + describe('mapQueryStatusFilter', () => { + it.each` + active | inactive | statusFilter + ${true} | ${true} | ${'all'} + ${undefined} | ${undefined} | ${'all'} + ${false} | ${false} | ${'none'} + ${true} | ${false} | ${'active'} + ${true} | ${undefined} | ${'active'} + ${undefined} | ${false} | ${'active'} + ${false} | ${true} | ${'inactive'} + ${undefined} | ${true} | ${'inactive'} + ${false} | ${undefined} | ${'inactive'} + `( + 'returns "$statusFilter" when active is $active, and inactive is $inactive', + ({ active, inactive, statusFilter }) => { + expect(mapQueryStatusFilter(active, inactive)).toBe(statusFilter) + } + ) + }) }) diff --git a/src/core/utils.ts b/src/core/utils.ts index 35b1a85db1..49b310ed55 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -68,6 +68,8 @@ export type Updater = | TOutput | DataUpdateFunction +export type QueryStatusFilter = 'all' | 'active' | 'inactive' | 'none' + // UTILS export const isServer = typeof window === 'undefined' @@ -164,6 +166,25 @@ export function parseFilterArgs< : [arg1 || {}, arg2]) as [TFilters, TOptions] } +export function mapQueryStatusFilter( + active?: boolean, + inactive?: boolean +): QueryStatusFilter { + if ( + (active === true && inactive === true) || + (active == null && inactive == null) + ) { + return 'all' + } else if (active === false && inactive === false) { + return 'none' + } else { + // At this point, active|inactive can only be true|false or false|true + // so, when only one value is provided, the missing one has to be the negated value + const isActive = active ?? !inactive + return isActive ? 'active' : 'inactive' + } +} + export function matchQuery( filters: QueryFilters, query: Query @@ -188,16 +209,18 @@ export function matchQuery( } } - let isActive + const queryStatusFilter = mapQueryStatusFilter(active, inactive) - if (inactive === false || (active && !inactive)) { - isActive = true - } else if (active === false || (inactive && !active)) { - isActive = false - } - - if (typeof isActive === 'boolean' && query.isActive() !== isActive) { + if (queryStatusFilter === 'none') { return false + } else if (queryStatusFilter !== 'all') { + const isActive = query.isActive() + if (queryStatusFilter === 'active' && !isActive) { + return false + } + if (queryStatusFilter === 'inactive' && isActive) { + return false + } } if (typeof stale === 'boolean' && query.isStale() !== stale) {