Skip to content

Commit

Permalink
feat: better query filters (#2938)
Browse files Browse the repository at this point in the history
  • Loading branch information
TkDodo committed Nov 14, 2021
1 parent e892557 commit 56598fb
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 144 deletions.
16 changes: 7 additions & 9 deletions docs/src/pages/guides/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,23 @@ A query filter is an object with certain conditions to match a query with:
await queryClient.cancelQueries()

// Remove all inactive queries that begin with `posts` in the key
queryClient.removeQueries('posts', { inactive: true })
queryClient.removeQueries('posts', { type: 'inactive' })

// Refetch all active queries
await queryClient.refetchQueries({ active: true })
await queryClient.refetchQueries({ type: 'active' })

// Refetch all active queries that begin with `posts` in the key
await queryClient.refetchQueries('posts', { active: true })
await queryClient.refetchQueries('posts', { type: 'active' })
```

A query filter object supports the following properties:

- `exact?: boolean`
- If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed.
- `active?: boolean`
- When set to `true` it will match active queries.
- When set to `false` it will match inactive queries.
- `inactive?: boolean`
- When set to `true` it will match inactive queries.
- When set to `false` it will match active queries.
- `type?: 'active' | 'inactive' | 'all'`
- Defaults to `all`
- When set to `active` it will match active queries.
- When set to `inactive` it will match inactive queries.
- `stale?: boolean`
- When set to `true` it will match stale queries.
- When set to `false` it will match fresh queries.
Expand Down
48 changes: 48 additions & 0 deletions docs/src/pages/guides/migrating-to-react-query-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,51 @@ queryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })
```

> Note: There is no change in behaviour for automatically triggered fetches, e.g. because a query mounts or because of a window focus refetch.
### Query Filters

A [query filter](../guides/filters) is an object with certain conditions to match a query. Historically, the filter options have mostly been a combination of boolean flags. However, combining those flags can lead to impossible states. Specifically:

```
active?: boolean
- When set to true it will match active queries.
- When set to false it will match inactive queries.
inactive?: boolean
- When set to true it will match inactive queries.
- When set to false it will match active queries.
```

Those flags don't work well when used together, because they are mutually exclusive. Setting `false` for both flags could match all queries, judging from the description, or no queries, which doesn't make much sense.

With v4, those filters have been combined into a single filter to better show the intent:

```diff
- active?: boolean
- inactive?: boolean
+ type?: 'active' | 'inactive' | 'all'
```

The filter defaults to `all`, and you can choose to only match `active` or `inactive` queries.

#### refetchActive / refetchInactive

[queryClient.invalidateQueries](../reference/QueryClient#queryclientinvalidatequeries) had two additional, similar flags:

```
refetchActive: Boolean
- Defaults to true
- When set to false, queries that match the refetch predicate and are actively being rendered via useQuery and friends will NOT be refetched in the background, and only marked as invalid.
refetchInactive: Boolean
- Defaults to false
- When set to true, queries that match the refetch predicate and are not being rendered via useQuery and friends will be both marked as invalid and also refetched in the background
```

For the same reason, those have also been combined:

```diff
- active?: boolean
- inactive?: boolean
+ refetchType?: 'active' | 'inactive' | 'all' | 'none'
```

This flag defaults to `active` because `refetchActive` defaulted to `true`. This means we also need a way to tell `invalidateQueries` to not refetch at all, which is why a fourth option (`none`) is also allowed here.
23 changes: 11 additions & 12 deletions docs/src/pages/reference/QueryClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,27 +268,26 @@ queryClient.setQueriesData(queryKey | filters, updater)

The `invalidateQueries` method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background.

- If you **do not want active queries to refetch**, and simply be marked as invalid, you can use the `refetchActive: false` option.
- If you **want inactive queries to refetch** as well, use the `refetchInactive: true` option
- If you **do not want active queries to refetch**, and simply be marked as invalid, you can use the `refetchType: 'none'` option.
- If you **want inactive queries to refetch** as well, use the `refetchTye: 'all'` option

```js
await queryClient.invalidateQueries('posts', {
exact,
refetchActive: true,
refetchInactive: false
refetchType: 'active',
}, { throwOnError, cancelRefetch })
```

**Options**

- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
- `refetchActive: Boolean`
- Defaults to `true`
- When set to `false`, queries that match the refetch predicate and are actively being rendered via `useQuery` and friends will NOT be refetched in the background, and only marked as invalid.
- `refetchInactive: Boolean`
- Defaults to `false`
- When set to `true`, queries that match the refetch predicate and are not being rendered via `useQuery` and friends will be both marked as invalid and also refetched in the background
- `refetchType?: 'active' | 'inactive' | 'all' | 'none'`
- Defaults to `'active'`
- When set to `active`, only queries that match the refetch predicate and are actively being rendered via `useQuery` and friends will be refetched in the background.
- When set to `inactive`, only queries that match the refetch predicate and are NOT actively being rendered via `useQuery` and friends will be refetched in the background.
- When set to `all`, all queries that match the refetch predicate will be refetched in the background.
- When set to `none`, no queries will be refetched, and those that match the refetch predicate will be marked as invalid only.
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
- Use this function to specify which pages should be refetched
Expand All @@ -314,10 +313,10 @@ await queryClient.refetchQueries()
await queryClient.refetchQueries({ stale: true })

// refetch all active queries partially matching a query key:
await queryClient.refetchQueries(['posts'], { active: true })
await queryClient.refetchQueries(['posts'], { type: 'active' })

// refetch all active queries exactly matching a query key:
await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
await queryClient.refetchQueries(['posts', 1], { type: 'active', exact: true })
```

**Options**
Expand Down
18 changes: 9 additions & 9 deletions src/core/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ export class QueryClient {
const queryCache = this.queryCache

const refetchFilters: RefetchQueryFilters = {
type: 'active',
...filters,
active: true,
}

return notifyManager.batch(() => {
Expand Down Expand Up @@ -255,18 +255,18 @@ export class QueryClient {
): Promise<void> {
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)

const refetchFilters: RefetchQueryFilters = {
...filters,
// 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(() => {
this.queryCache.findAll(filters).forEach(query => {
query.invalidate()
})

if (filters?.refetchType === 'none') {
return Promise.resolve()
}
const refetchFilters: RefetchQueryFilters = {
...filters,
type: filters?.refetchType ?? filters?.type ?? 'active',
}
return this.refetchQueries(refetchFilters, options)
})
}
Expand Down
8 changes: 4 additions & 4 deletions src/core/tests/queriesObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ describe('queriesObserver', () => {
observer.setQueries([{ queryKey: key2, queryFn: queryFn2 }])
await sleep(1)
const queryCache = queryClient.getQueryCache()
expect(queryCache.find(key1, { active: true })).toBeUndefined()
expect(queryCache.find(key2, { active: true })).toBeDefined()
expect(queryCache.find(key1, { type: 'active' })).toBeUndefined()
expect(queryCache.find(key2, { type: 'active' })).toBeDefined()
unsubscribe()
expect(queryCache.find(key1, { active: true })).toBeUndefined()
expect(queryCache.find(key2, { active: true })).toBeUndefined()
expect(queryCache.find(key1, { type: 'active' })).toBeUndefined()
expect(queryCache.find(key2, { type: 'active' })).toBeUndefined()
expect(results.length).toBe(6)
expect(results[0]).toMatchObject([
{ status: 'idle', data: undefined },
Expand Down
18 changes: 9 additions & 9 deletions src/core/tests/queryCache.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ describe('queryCache', () => {
expect(queryCache.findAll([key1])).toEqual([query1])
expect(queryCache.findAll()).toEqual([query1, query2, query3, query4])
expect(queryCache.findAll({})).toEqual([query1, query2, query3, query4])
expect(queryCache.findAll(key1, { active: false })).toEqual([query1])
expect(queryCache.findAll(key1, { active: true })).toEqual([])
expect(queryCache.findAll(key1, { type: 'inactive' })).toEqual([query1])
expect(queryCache.findAll(key1, { type: 'active' })).toEqual([])
expect(queryCache.findAll(key1, { stale: true })).toEqual([])
expect(queryCache.findAll(key1, { stale: false })).toEqual([query1])
expect(queryCache.findAll(key1, { stale: false, active: true })).toEqual(
[]
)
expect(
queryCache.findAll(key1, { stale: false, active: false })
queryCache.findAll(key1, { stale: false, type: 'active' })
).toEqual([])
expect(
queryCache.findAll(key1, { stale: false, type: 'inactive' })
).toEqual([query1])
expect(
queryCache.findAll(key1, {
stale: false,
active: false,
type: 'inactive',
exact: true,
})
).toEqual([query1])
Expand All @@ -128,8 +128,8 @@ describe('queryCache', () => {
query3,
])
expect(queryCache.findAll([{ a: 'a' }], { stale: true })).toEqual([])
expect(queryCache.findAll([{ a: 'a' }], { active: true })).toEqual([])
expect(queryCache.findAll([{ a: 'a' }], { inactive: true })).toEqual([
expect(queryCache.findAll([{ a: 'a' }], { type: 'active' })).toEqual([])
expect(queryCache.findAll([{ a: 'a' }], { type: 'inactive' })).toEqual([
query3,
])
expect(
Expand Down
51 changes: 16 additions & 35 deletions src/core/tests/queryClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ describe('queryClient', () => {
staleTime: Infinity,
})
const unsubscribe = observer.subscribe()
await queryClient.refetchQueries({ active: true, stale: false })
await queryClient.refetchQueries({ type: 'active', stale: false })
unsubscribe()
expect(queryFn1).toHaveBeenCalledTimes(2)
expect(queryFn2).toHaveBeenCalledTimes(1)
Expand Down Expand Up @@ -713,7 +713,7 @@ describe('queryClient', () => {
})
const unsubscribe = observer.subscribe()
await queryClient.refetchQueries(
{ active: true, stale: true },
{ type: 'active', stale: true },
{ cancelRefetch: false }
)
unsubscribe()
Expand Down Expand Up @@ -753,7 +753,7 @@ describe('queryClient', () => {
staleTime: Infinity,
})
const unsubscribe = observer.subscribe()
await queryClient.refetchQueries({ active: true, inactive: true })
await queryClient.refetchQueries({ type: 'all' })
unsubscribe()
expect(queryFn1).toHaveBeenCalledTimes(2)
expect(queryFn2).toHaveBeenCalledTimes(2)
Expand All @@ -772,7 +772,7 @@ describe('queryClient', () => {
staleTime: Infinity,
})
const unsubscribe = observer.subscribe()
await queryClient.refetchQueries({ active: true })
await queryClient.refetchQueries({ type: 'active' })
unsubscribe()
expect(queryFn1).toHaveBeenCalledTimes(2)
expect(queryFn2).toHaveBeenCalledTimes(1)
Expand All @@ -791,31 +791,12 @@ describe('queryClient', () => {
staleTime: Infinity,
})
const unsubscribe = observer.subscribe()
await queryClient.refetchQueries({ inactive: true })
await queryClient.refetchQueries({ type: 'inactive' })
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)
})

test('should throw an error if throwOnError option is set to true', async () => {
const consoleMock = mockConsoleError()
const key1 = queryKey()
Expand Down Expand Up @@ -880,7 +861,7 @@ describe('queryClient', () => {
expect(queryFn2).toHaveBeenCalledTimes(1)
})

test('should not refetch active queries when "refetchActive" is false', async () => {
test('should not refetch active queries when "refetch" is "none"', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = jest.fn()
Expand All @@ -894,14 +875,14 @@ describe('queryClient', () => {
})
const unsubscribe = observer.subscribe()
queryClient.invalidateQueries(key1, {
refetchActive: false,
refetchType: 'none',
})
unsubscribe()
expect(queryFn1).toHaveBeenCalledTimes(1)
expect(queryFn2).toHaveBeenCalledTimes(1)
})

test('should refetch inactive queries when "refetchInactive" is true', async () => {
test('should refetch inactive queries when "refetch" is "inactive"', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = jest.fn()
Expand All @@ -916,14 +897,14 @@ describe('queryClient', () => {
})
const unsubscribe = observer.subscribe()
queryClient.invalidateQueries(key1, {
refetchInactive: true,
refetchType: 'inactive',
})
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 () => {
test('should refetch active and inactive queries when "refetch" is "all"', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = jest.fn()
Expand All @@ -936,12 +917,12 @@ describe('queryClient', () => {
staleTime: Infinity,
})
const unsubscribe = observer.subscribe()
queryClient.invalidateQueries(key1, {
active: false,
queryClient.invalidateQueries({
refetchType: 'all',
})
unsubscribe()
expect(queryFn1).toHaveBeenCalledTimes(1)
expect(queryFn2).toHaveBeenCalledTimes(1)
expect(queryFn1).toHaveBeenCalledTimes(2)
expect(queryFn2).toHaveBeenCalledTimes(2)
})

test('should cancel ongoing fetches if cancelRefetch option is set (default value)', async () => {
Expand Down Expand Up @@ -1123,7 +1104,7 @@ describe('queryClient', () => {

await queryClient.invalidateQueries({
queryKey: key,
refetchInactive: true,
refetchType: 'all',
refetchPage: (page, _, allPages) => {
return page === allPages[0]
},
Expand Down Expand Up @@ -1155,7 +1136,7 @@ describe('queryClient', () => {

await queryClient.resetQueries({
queryKey: key,
inactive: true,
type: 'inactive',
refetchPage: (page, _, allPages) => {
return page === allPages[0]
},
Expand Down
Loading

0 comments on commit 56598fb

Please sign in to comment.